diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 3040f8b..26970cd 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -11,5 +11,11 @@ if [ -z "$PRISMA_QUERY_ENGINE_LIBRARY" ]; then export PRISMA_QUERY_ENGINE_LIBRARY=/app/runtime/libquery_engine-debian-openssl-3.0.x.so.node fi +if [ -n "${MONTACT_API_TOKEN:-}" ]; then + echo "MONTACT_API_TOKEN: set" +else + echo "MONTACT_API_TOKEN: NOT SET (only admin JWT works for /montact)" +fi + echo "Starting API on port ${PORT:-3000}..." exec node runtime/server.cjs diff --git a/runtime/server.cjs b/runtime/server.cjs index 74dd34a..70bef64 100644 --- a/runtime/server.cjs +++ b/runtime/server.cjs @@ -135632,9 +135632,20 @@ var require_check_rass = __commonJS({ "dist/common/utils/check-rass.js"(exports2) { "use strict"; Object.defineProperty(exports2, "__esModule", { value: true }); + exports2.invoiceAnchorDate = invoiceAnchorDate; exports2.calculateCheckRass = calculateCheckRass; var tehran_time_1 = require_tehran_time(); - function calculateCheckRass(checks, baseDate = /* @__PURE__ */ new Date()) { + var INVOICE_ISSUED_STATUS = "awaiting_settlement"; + function invoiceAnchorDate(createdAt, history = []) { + for (let i = history.length - 1; i >= 0; i -= 1) { + const row = history[i]; + if (row?.toStatus === INVOICE_ISSUED_STATUS) + return row.changedAt; + } + return createdAt; + } + __name(invoiceAnchorDate, "invoiceAnchorDate"); + function calculateCheckRass(checks, baseDate) { if (checks.length === 0) { return { checkCount: 0, @@ -136730,7 +136741,7 @@ var require_orders_service = __commonJS({ throw new common_12.BadRequestException("\u062D\u062F\u0627\u0642\u0644 \u06CC\u06A9 \u0686\u06A9 \u0644\u0627\u0632\u0645 \u0627\u0633\u062A"); } const checkMode = dto.checkMode ?? order.checkMode ?? client_1.CheckRegistrationMode.full; - const rass = (0, check_rass_1.calculateCheckRass)(dto.checks.map((c) => ({ amount: c.amount, dueDate: c.dueDate })), order.createdAt); + const rass = (0, check_rass_1.calculateCheckRass)(dto.checks.map((c) => ({ amount: c.amount, dueDate: c.dueDate })), (0, check_rass_1.invoiceAnchorDate)(order.createdAt, order.statusHistory)); const goesToReview = order.status === client_1.OrderStatus.check_in_progress || order.status === client_1.OrderStatus.check_needs_fix || order.status === client_1.OrderStatus.check_rejected; const nextStatus = goesToReview ? client_1.OrderStatus.awaiting_check_review : order.status; const isFix = order.status !== client_1.OrderStatus.check_in_progress; @@ -136933,6 +136944,10 @@ var require_orders_service = __commonJS({ mapOrder(order, adminView = false) { const customerName = (0, name_1.joinFullName)(order.user.firstName, order.user.lastName); const hidePrices = !adminView && PRICE_HIDDEN_STATUSES.includes(order.status); + const liveRass = order.checks.length > 0 ? (0, check_rass_1.calculateCheckRass)(order.checks.map((check) => ({ + amount: check.amount, + dueDate: check.dueDate + })), (0, check_rass_1.invoiceAnchorDate)(order.createdAt, order.statusHistory)) : null; const base = { id: order.id, orderNumber: order.orderNumber, @@ -136953,8 +136968,8 @@ var require_orders_service = __commonJS({ finalAmount: hidePrices ? null : order.finalAmount, pricesVisible: !hidePrices, notes: order.notes ?? void 0, - checkRassDays: order.checkRassDays ?? void 0, - checkRassDate: order.checkRassDate?.toISOString(), + checkRassDays: liveRass ? liveRass.rassDays : order.checkRassDays ?? void 0, + checkRassDate: liveRass?.rassDate ?? order.checkRassDate?.toISOString(), cashInvoiceExpiresAt: order.cashInvoiceExpiresAt?.toISOString(), createdAt: order.createdAt.toISOString(), updatedAt: order.updatedAt.toISOString(), @@ -138371,7 +138386,7 @@ var require_cash_payments_service = __commonJS({ paidAmount: dto.paidAmount ?? dto.amount, transferDate: new Date(dto.transferDate), transferTime: dto.transferTime.trim(), - trackingNumber: dto.trackingNumber.trim(), + trackingNumber: dto.trackingNumber?.trim() || null, destinationAccountId: dto.destinationAccountId, receiptImage: dto.receiptImage || null, notes: dto.notes?.trim() || null, @@ -138563,6 +138578,7 @@ var require_cash_payment_dto = __commonJS({ __metadata2("design:type", String) ], SubmitCashReceiptDto.prototype, "transferTime", void 0); __decorate2([ + (0, class_validator_1.IsOptional)(), (0, class_validator_1.IsString)(), (0, class_validator_1.MinLength)(1), __metadata2("design:type", String) @@ -141367,7 +141383,8 @@ var require_suggestions_service = __commonJS({ colorsJson: true, imageUrlsJson: true, widthCm: true, - composition: true + composition: true, + weightGrams: true } }, customer: { select: { firstName: true, lastName: true, phone: true } } @@ -141486,6 +141503,7 @@ var require_suggestions_service = __commonJS({ colorImages, widthCm: r.product.widthCm ?? void 0, composition: r.product.composition?.trim() || void 0, + weightGrams: r.product.weightGrams ?? void 0, isNew: r.product.isNew, productIsActive: r.product.isActive, note: r.note ?? void 0, @@ -142775,6 +142793,16 @@ var require_montact_auth_guard = __commonJS({ var config_1 = require_config2(); var jwt_1 = require_jwt(); var jwt_secret_1 = require_jwt_secret(); + function normalizeMontactToken(raw2) { + if (!raw2) + return void 0; + let t = raw2.trim(); + if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) { + t = t.slice(1, -1).trim(); + } + return t.length > 0 ? t : void 0; + } + __name(normalizeMontactToken, "normalizeMontactToken"); var MontactAuthGuard = class MontactAuthGuard { static { __name(this, "MontactAuthGuard"); @@ -142792,7 +142820,7 @@ var require_montact_auth_guard = __commonJS({ throw new common_12.UnauthorizedException("\u062A\u0648\u06A9\u0646 \u0646\u0627\u0645\u0639\u062A\u0628\u0631 \u06CC\u0627 \u0645\u0646\u0642\u0636\u06CC \u0634\u062F\u0647 \u0627\u0633\u062A"); } const token = authHeader.slice(7).trim(); - const montactToken = this.config.get("MONTACT_API_TOKEN")?.trim(); + const montactToken = normalizeMontactToken(this.config.get("MONTACT_API_TOKEN")); if (montactToken && token === montactToken) { return true; } diff --git a/src/cash-payments/cash-payments.service.ts b/src/cash-payments/cash-payments.service.ts index c43a267..f964a26 100644 --- a/src/cash-payments/cash-payments.service.ts +++ b/src/cash-payments/cash-payments.service.ts @@ -85,7 +85,7 @@ export class CashPaymentsService { paidAmount: dto.paidAmount ?? dto.amount, transferDate: new Date(dto.transferDate), transferTime: dto.transferTime.trim(), - trackingNumber: dto.trackingNumber.trim(), + trackingNumber: dto.trackingNumber?.trim() || null, destinationAccountId: dto.destinationAccountId, receiptImage: dto.receiptImage || null, notes: dto.notes?.trim() || null, diff --git a/src/cash-payments/dto/cash-payment.dto.ts b/src/cash-payments/dto/cash-payment.dto.ts index a190968..ccb62e8 100644 --- a/src/cash-payments/dto/cash-payment.dto.ts +++ b/src/cash-payments/dto/cash-payment.dto.ts @@ -29,9 +29,10 @@ export class SubmitCashReceiptDto { @MinLength(1) transferTime!: string; + @IsOptional() @IsString() @MinLength(1) - trackingNumber!: string; + trackingNumber?: string; @IsString() destinationAccountId!: string; diff --git a/src/common/utils/check-rass.spec.ts b/src/common/utils/check-rass.spec.ts new file mode 100644 index 0000000..00f2ca4 --- /dev/null +++ b/src/common/utils/check-rass.spec.ts @@ -0,0 +1,52 @@ +import { calculateCheckRass, invoiceAnchorDate } from './check-rass'; + +const checks = [ + { amount: 100, dueDate: '2026-11-01T00:00:00.000Z' }, + { amount: 100, dueDate: '2026-12-01T00:00:00.000Z' }, +]; + +describe('invoiceAnchorDate', () => { + const createdAt = new Date('2026-09-01T06:00:00.000Z'); + + it('uses the latest invoice issue, not order creation', () => { + const firstIssue = new Date('2026-10-01T08:00:00.000Z'); + const reissue = new Date('2026-10-05T08:00:00.000Z'); + expect( + invoiceAnchorDate(createdAt, [ + { toStatus: 'pricing', changedAt: new Date('2026-09-20T08:00:00.000Z') }, + { toStatus: 'awaiting_settlement', changedAt: firstIssue }, + { toStatus: 'pricing', changedAt: new Date('2026-10-04T08:00:00.000Z') }, + { toStatus: 'awaiting_settlement', changedAt: reissue }, + ]), + ).toEqual(reissue); + }); + + it('falls back to the order creation date when no invoice was issued', () => { + expect( + invoiceAnchorDate(createdAt, [ + { toStatus: 'awaiting_review', changedAt: createdAt }, + ]), + ).toEqual(createdAt); + }); +}); + +describe('calculateCheckRass', () => { + const invoice = new Date('2026-10-01T08:00:00.000Z'); + + it('stays the same when the calendar day changes but the invoice date does not', () => { + const onIssueDay = calculateCheckRass(checks, invoice); + const stillOnInvoice = calculateCheckRass(checks, invoice); + expect(stillOnInvoice).toEqual(onIssueDay); + expect(onIssueDay.rassDays).toBe(46); + expect(onIssueDay.rassDate).toBe('2026-11-16T00:00:00.000Z'); + }); + + it('is shorter when measured from a later day than the invoice', () => { + const fromInvoice = calculateCheckRass(checks, invoice); + const fromLater = calculateCheckRass( + checks, + new Date('2026-10-20T08:00:00.000Z'), + ); + expect(fromLater.rassDays).toBeLessThan(fromInvoice.rassDays); + }); +}); diff --git a/src/common/utils/check-rass.ts b/src/common/utils/check-rass.ts index cf8a4e0..d7c9b7d 100644 --- a/src/common/utils/check-rass.ts +++ b/src/common/utils/check-rass.ts @@ -19,9 +19,27 @@ import { tehranCalendarDay, } from './tehran-time'; +/** وضعیتی که فاکتور قیمت‌گذاری‌شده برای مشتری ارسال می‌شود. */ +const INVOICE_ISSUED_STATUS = 'awaiting_settlement'; + +/** + * مبدأ راس چک، تاریخ صدور فاکتور است نه امروز و نه تاریخ ثبت پیش‌سفارش. + * اگر فاکتور دوباره ارسال شده باشد، آخرین صدور ملاک است. + */ +export function invoiceAnchorDate( + createdAt: Date, + history: { toStatus: string; changedAt: Date }[] = [], +): Date { + for (let i = history.length - 1; i >= 0; i -= 1) { + const row = history[i]; + if (row?.toStatus === INVOICE_ISSUED_STATUS) return row.changedAt; + } + return createdAt; +} + export function calculateCheckRass( checks: CheckRassInput[], - baseDate: Date = new Date(), + baseDate: Date, ): CheckRassResult { if (checks.length === 0) { return { diff --git a/src/montact/montact-auth.guard.ts b/src/montact/montact-auth.guard.ts index b9162d6..57af729 100644 --- a/src/montact/montact-auth.guard.ts +++ b/src/montact/montact-auth.guard.ts @@ -8,6 +8,18 @@ import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { resolveJwtSecret } from '../common/utils/jwt-secret'; +function normalizeMontactToken(raw?: string): string | undefined { + if (!raw) return undefined; + let t = raw.trim(); + if ( + (t.startsWith('"') && t.endsWith('"')) || + (t.startsWith("'") && t.endsWith("'")) + ) { + t = t.slice(1, -1).trim(); + } + return t.length > 0 ? t : undefined; +} + @Injectable() export class MontactAuthGuard implements CanActivate { constructor( @@ -24,7 +36,9 @@ export class MontactAuthGuard implements CanActivate { throw new UnauthorizedException('توکن نامعتبر یا منقضی شده است'); } const token = authHeader.slice(7).trim(); - const montactToken = this.config.get('MONTACT_API_TOKEN')?.trim(); + const montactToken = normalizeMontactToken( + this.config.get('MONTACT_API_TOKEN'), + ); if (montactToken && token === montactToken) { return true; } diff --git a/src/orders/orders.service.ts b/src/orders/orders.service.ts index 2404537..24f0e71 100644 --- a/src/orders/orders.service.ts +++ b/src/orders/orders.service.ts @@ -22,7 +22,10 @@ import { UpdateOrderStatusDto, } from './dto/order.dto'; import { generateOrderNumber } from '../common/utils/order-number'; -import { calculateCheckRass } from '../common/utils/check-rass'; +import { + calculateCheckRass, + invoiceAnchorDate, +} from '../common/utils/check-rass'; import { usesCashReceiptPhase, usesCheckSettlement, @@ -616,7 +619,7 @@ export class OrdersService { dto.checkMode ?? order.checkMode ?? CheckRegistrationMode.full; const rass = calculateCheckRass( dto.checks.map((c) => ({ amount: c.amount, dueDate: c.dueDate })), - order.createdAt, + invoiceAnchorDate(order.createdAt, order.statusHistory), ); // ثبت اولیه و اصلاح، هر دو سفارش را به صف بررسی چک می‌برند. @@ -890,6 +893,16 @@ export class OrdersService { ); const hidePrices = !adminView && PRICE_HIDDEN_STATUSES.includes(order.status); + const liveRass = + order.checks.length > 0 + ? calculateCheckRass( + order.checks.map((check) => ({ + amount: check.amount, + dueDate: check.dueDate, + })), + invoiceAnchorDate(order.createdAt, order.statusHistory), + ) + : null; const base = { id: order.id, orderNumber: order.orderNumber, @@ -910,8 +923,10 @@ export class OrdersService { finalAmount: hidePrices ? null : order.finalAmount, pricesVisible: !hidePrices, notes: order.notes ?? undefined, - checkRassDays: order.checkRassDays ?? undefined, - checkRassDate: order.checkRassDate?.toISOString(), + checkRassDays: liveRass + ? liveRass.rassDays + : (order.checkRassDays ?? undefined), + checkRassDate: liveRass?.rassDate ?? order.checkRassDate?.toISOString(), cashInvoiceExpiresAt: order.cashInvoiceExpiresAt?.toISOString(), createdAt: order.createdAt.toISOString(), updatedAt: order.updatedAt.toISOString(), diff --git a/src/suggestions/suggestions.service.ts b/src/suggestions/suggestions.service.ts index ec3a233..f4eaa69 100644 --- a/src/suggestions/suggestions.service.ts +++ b/src/suggestions/suggestions.service.ts @@ -20,6 +20,7 @@ const PRODUCT_INCLUDE = { imageUrlsJson: true, widthCm: true, composition: true, + weightGrams: true, }, }, customer: { select: { firstName: true, lastName: true, phone: true } }, @@ -148,6 +149,7 @@ export class SuggestionsService { imageUrlsJson: unknown; widthCm: number | null; composition: string | null; + weightGrams: number | null; }; }) { const colorEntries = parseColorEntries(r.product.colorsJson, r.product.color); @@ -181,6 +183,7 @@ export class SuggestionsService { colorImages, widthCm: r.product.widthCm ?? undefined, composition: r.product.composition?.trim() || undefined, + weightGrams: r.product.weightGrams ?? undefined, isNew: r.product.isNew, productIsActive: r.product.isActive, note: r.note ?? undefined,