Compare commits
14 Commits
59ee1fbe33
...
uat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b771bb124c | ||
|
|
f1cfe85dfe | ||
|
|
dcfd93c545 | ||
|
|
c8c6e0ebc3 | ||
|
|
e94644548e | ||
|
|
de930e470b | ||
|
|
5b168f5682 | ||
|
|
ab4eeb7ce6 | ||
|
|
67081dbe76 | ||
|
|
7ee821d46e | ||
|
|
78d403dcff | ||
|
|
7c90f118ed | ||
|
|
1eb61000c3 | ||
|
|
82cac36a48 |
@@ -1,4 +1,5 @@
|
|||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
# cache-bust-v3
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=development
|
ENV NODE_ENV=development
|
||||||
@@ -8,6 +9,7 @@ COPY packages/shared/package.json ./packages/shared/
|
|||||||
COPY packages/db/package.json ./packages/db/
|
COPY packages/db/package.json ./packages/db/
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
|
RUN echo "bust-20260415-3" > /tmp/.cachebust && rm /tmp/.cachebust
|
||||||
COPY packages/shared/ ./packages/shared/
|
COPY packages/shared/ ./packages/shared/
|
||||||
COPY packages/db/ ./packages/db/
|
COPY packages/db/ ./packages/db/
|
||||||
COPY nest-cli.json ./
|
COPY nest-cli.json ./
|
||||||
@@ -30,8 +32,9 @@ RUN node -e "const p=require('./packages/shared/package.json');p.main='./dist/in
|
|||||||
COPY --from=builder /app/packages/db/prisma ./packages/db/prisma
|
COPY --from=builder /app/packages/db/prisma ./packages/db/prisma
|
||||||
COPY --from=builder /app/packages/db/package.json ./packages/db/
|
COPY --from=builder /app/packages/db/package.json ./packages/db/
|
||||||
COPY --from=builder /app/packages/db/src ./packages/db/src
|
COPY --from=builder /app/packages/db/src ./packages/db/src
|
||||||
|
COPY --from=builder /app/packages/db/prisma/seed.ts ./packages/db/prisma/seed.ts
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
ENTRYPOINT ["dumb-init", "--"]
|
ENTRYPOINT ["dumb-init", "--"]
|
||||||
CMD ["sh", "-c", "cd packages/db && npx prisma migrate deploy && cd /app && node dist/main"]
|
CMD ["sh", "-c", "cd packages/db && npx prisma migrate deploy && echo 'Seeding database...' && npx tsx prisma/seed.ts && cd /app && node dist/main"]
|
||||||
|
|||||||
5
fix-schema.js
Normal file
5
fix-schema.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const { PrismaClient } = require("@prisma/client");
|
||||||
|
const p = new PrismaClient();
|
||||||
|
p.$executeRawUnsafe("ALTER TABLE users ALTER COLUMN \"tenantId\" DROP NOT NULL")
|
||||||
|
.then(() => { console.log("tenantId nullable OK"); return p.$disconnect(); })
|
||||||
|
.catch(e => { console.log("Error:", e.message.substring(0, 100)); return p.$disconnect(); });
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- AlterColumn: make tenantId nullable for platform-level users (superadmin)
|
||||||
|
ALTER TABLE "users" ALTER COLUMN "tenantId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- Drop existing FK and recreate with ON DELETE SET NULL
|
||||||
|
ALTER TABLE "users" DROP CONSTRAINT "users_tenantId_fkey";
|
||||||
|
ALTER TABLE "users" ADD CONSTRAINT "users_tenantId_fkey"
|
||||||
|
FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Recreate unique index to allow multiple NULL tenantId values
|
||||||
|
DROP INDEX IF EXISTS "users_tenantId_email_key";
|
||||||
|
CREATE UNIQUE INDEX "users_tenantId_email_key" ON "users"("tenantId", "email") WHERE "tenantId" IS NOT NULL;
|
||||||
|
CREATE UNIQUE INDEX "users_email_key" ON "users"("email") WHERE "tenantId" IS NULL;
|
||||||
@@ -57,6 +57,14 @@ async function hashPassword(password: string): Promise<string> {
|
|||||||
async function main() {
|
async function main() {
|
||||||
console.log('Seeding database...');
|
console.log('Seeding database...');
|
||||||
|
|
||||||
|
// Ensure tenantId is nullable (in case migration hasn't been applied)
|
||||||
|
try {
|
||||||
|
await prisma.$executeRawUnsafe(`ALTER TABLE "users" ALTER COLUMN "tenantId" DROP NOT NULL`);
|
||||||
|
console.log('Made tenantId nullable');
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log('tenantId already nullable or error:', e.message?.substring(0, 80));
|
||||||
|
}
|
||||||
|
|
||||||
// Clean existing data (order matters for FK constraints)
|
// Clean existing data (order matters for FK constraints)
|
||||||
await prisma.journalLine.deleteMany();
|
await prisma.journalLine.deleteMany();
|
||||||
await prisma.journalEntry.deleteMany();
|
await prisma.journalEntry.deleteMany();
|
||||||
@@ -101,9 +109,22 @@ async function main() {
|
|||||||
console.log(`Tenant: ${tenant.name}`);
|
console.log(`Tenant: ${tenant.name}`);
|
||||||
|
|
||||||
// ─── Super Admin (platform-level, no tenant) ───────────
|
// ─── Super Admin (platform-level, no tenant) ───────────
|
||||||
const superAdmin = await prisma.user.create({
|
// If tenantId column is NOT NULL (migration not applied), use tenant.id as fallback
|
||||||
data: { tenantId: null, email: 'superadmin@fiberops.dev', password: await hashPassword('admin123!'), firstName: 'Super', lastName: 'Admin' },
|
let superAdmin;
|
||||||
});
|
try {
|
||||||
|
superAdmin = await prisma.user.create({
|
||||||
|
data: { tenantId: null, email: 'superadmin@fiberops.dev', password: await hashPassword('admin123!'), firstName: 'Super', lastName: 'Admin' },
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.code === 'P2002' || e?.message?.includes('Null constraint')) {
|
||||||
|
console.log('Note: tenantId is NOT NULL, creating superadmin with tenant binding');
|
||||||
|
superAdmin = await prisma.user.create({
|
||||||
|
data: { tenantId: tenant.id, email: 'superadmin@fiberops.dev', password: await hashPassword('admin123!'), firstName: 'Super', lastName: 'Admin' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
await prisma.userRole.create({ data: { userId: superAdmin.id, role: 'super_admin' } });
|
await prisma.userRole.create({ data: { userId: superAdmin.id, role: 'super_admin' } });
|
||||||
console.log(`Super Admin: superadmin@fiberops.dev (super_admin)`);
|
console.log(`Super Admin: superadmin@fiberops.dev (super_admin)`);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class InvoiceService {
|
|||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
include: {
|
include: {
|
||||||
client: { select: { id: true, firstName: true, lastName: true, accountNumber: true } },
|
client: { select: { id: true, firstName: true, lastName: true, accountNumber: true, phone: true, latitude: true, longitude: true } },
|
||||||
_count: { select: { payments: true } },
|
_count: { select: { payments: true } },
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
@@ -83,7 +83,7 @@ export class InvoiceService {
|
|||||||
periodEnd: dueDate,
|
periodEnd: dueDate,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
client: { select: { id: true, firstName: true, lastName: true, accountNumber: true } },
|
client: { select: { id: true, firstName: true, lastName: true, accountNumber: true, phone: true, latitude: true, longitude: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export class PaymentService {
|
|||||||
// ─── Remittance (custodial clearing) ─────────────────────────
|
// ─── Remittance (custodial clearing) ─────────────────────────
|
||||||
|
|
||||||
async findRemittances(tenantId: string) {
|
async findRemittances(tenantId: string) {
|
||||||
return this.prisma.remittance.findMany({
|
const remittances = await this.prisma.remittance.findMany({
|
||||||
where: { tenantId },
|
where: { tenantId },
|
||||||
include: {
|
include: {
|
||||||
collector: { select: { id: true, firstName: true, lastName: true } },
|
collector: { select: { id: true, firstName: true, lastName: true } },
|
||||||
@@ -133,6 +133,29 @@ export class PaymentService {
|
|||||||
},
|
},
|
||||||
orderBy: { submittedAt: 'desc' },
|
orderBy: { submittedAt: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Collect all payment IDs across remittances
|
||||||
|
const paymentIds = remittances.flatMap((r) => r.payments.map((p) => p.paymentId));
|
||||||
|
if (paymentIds.length === 0) return remittances;
|
||||||
|
|
||||||
|
// Fetch full payment details in one query
|
||||||
|
const payments = await this.prisma.payment.findMany({
|
||||||
|
where: { id: { in: paymentIds } },
|
||||||
|
include: {
|
||||||
|
client: { select: { id: true, firstName: true, lastName: true, accountNumber: true } },
|
||||||
|
invoice: { select: { id: true, number: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const paymentMap = new Map(payments.map((p) => [p.id, p]));
|
||||||
|
|
||||||
|
// Attach full payment details to each remittance's join records
|
||||||
|
return remittances.map((r) => ({
|
||||||
|
...r,
|
||||||
|
payments: r.payments.map((rp) => ({
|
||||||
|
...rp,
|
||||||
|
payment: paymentMap.get(rp.paymentId) ?? null,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUnremittedPayments(tenantId: string, collectorId: string) {
|
async getUnremittedPayments(tenantId: string, collectorId: string) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { IsString, IsOptional, IsIn, MinLength, IsUUID } from 'class-validator';
|
import { IsString, IsOptional, IsIn, MinLength, IsUUID, IsNumber } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
export class UpdateTicketDto {
|
export class UpdateTicketDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -23,4 +24,14 @@ export class UpdateTicketDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsIn(['open', 'in_progress', 'resolved', 'cancelled'])
|
@IsIn(['open', 'in_progress', 'resolved', 'cancelled'])
|
||||||
status?: string;
|
status?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => value !== undefined && value !== null ? Number(value) : undefined)
|
||||||
|
@IsNumber()
|
||||||
|
latitude?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => value !== undefined && value !== null ? Number(value) : undefined)
|
||||||
|
@IsNumber()
|
||||||
|
longitude?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { CreateTicketDto } from './dto/create-ticket.dto';
|
import { CreateTicketDto } from './dto/create-ticket.dto';
|
||||||
import { UpdateTicketDto } from './dto/update-ticket.dto';
|
import { UpdateTicketDto } from './dto/update-ticket.dto';
|
||||||
import { NotificationService } from '../notification/notification.service';
|
|
||||||
|
|
||||||
export interface TicketResolvedEvent {
|
export interface TicketResolvedEvent {
|
||||||
ticketId: string;
|
ticketId: string;
|
||||||
@@ -21,10 +20,7 @@ export class TicketService {
|
|||||||
onTicketResolved: ((event: TicketResolvedEvent) => Promise<void>) | null =
|
onTicketResolved: ((event: TicketResolvedEvent) => Promise<void>) | null =
|
||||||
null;
|
null;
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
private readonly prisma: PrismaService,
|
|
||||||
private readonly notificationService: NotificationService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async findAll(tenantId: string, filters?: { clientId?: string; status?: string; type?: string }) {
|
async findAll(tenantId: string, filters?: { clientId?: string; status?: string; type?: string }) {
|
||||||
const db = this.prisma.forTenant(tenantId);
|
const db = this.prisma.forTenant(tenantId);
|
||||||
@@ -115,22 +111,13 @@ export class TicketService {
|
|||||||
...(dto.description !== undefined && { description: dto.description }),
|
...(dto.description !== undefined && { description: dto.description }),
|
||||||
...(dto.priority && { priority: dto.priority }),
|
...(dto.priority && { priority: dto.priority }),
|
||||||
...(dto.status && { status: dto.status }),
|
...(dto.status && { status: dto.status }),
|
||||||
|
...(dto.latitude !== undefined && { latitude: dto.latitude }),
|
||||||
|
...(dto.longitude !== undefined && { longitude: dto.longitude }),
|
||||||
},
|
},
|
||||||
}).then(async (ticket) => {
|
|
||||||
// Notify on status change
|
|
||||||
if (dto.status) {
|
|
||||||
this.notificationService.create(tenantId, {
|
|
||||||
type: 'in_app',
|
|
||||||
channel: 'ticket_update',
|
|
||||||
title: 'Ticket Updated',
|
|
||||||
message: `Ticket "${existing.title}" status changed to ${dto.status.replace(/_/g, ' ')}`,
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
return ticket;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async resolve(tenantId: string, id: string, resolvedById: string, coords?: { latitude?: number; longitude?: number }) {
|
async resolve(tenantId: string, id: string, resolvedById: string) {
|
||||||
const db = this.prisma.forTenant(tenantId);
|
const db = this.prisma.forTenant(tenantId);
|
||||||
const ticket = await db.ticket.findFirst({ where: { id } });
|
const ticket = await db.ticket.findFirst({ where: { id } });
|
||||||
|
|
||||||
@@ -155,14 +142,6 @@ export class TicketService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update client coordinates if this is an installation ticket with coords
|
|
||||||
if (coords?.latitude !== undefined && coords?.longitude !== undefined && ticket.clientId && ticket.type === 'installation') {
|
|
||||||
await this.prisma.client.update({
|
|
||||||
where: { id: ticket.clientId },
|
|
||||||
data: { latitude: coords.latitude, longitude: coords.longitude },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fire event for workflow automation
|
// Fire event for workflow automation
|
||||||
if (this.onTicketResolved && ticket.clientId) {
|
if (this.onTicketResolved && ticket.clientId) {
|
||||||
await this.onTicketResolved({
|
await this.onTicketResolved({
|
||||||
@@ -173,14 +152,6 @@ export class TicketService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify on ticket resolution
|
|
||||||
this.notificationService.create(tenantId, {
|
|
||||||
type: 'in_app',
|
|
||||||
channel: 'ticket_update',
|
|
||||||
title: 'Ticket Resolved',
|
|
||||||
message: `Ticket "${ticket.title}" has been resolved`,
|
|
||||||
}).catch(() => {});
|
|
||||||
|
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user