From daa559384f755548c3234e557d1ac93fea806534 Mon Sep 17 00:00:00 2001 From: kisinga Date: Thu, 16 Jul 2026 12:27:05 +0300 Subject: [PATCH] cashier flow fixes --- .../payment-allocation.service.spec.ts | 58 +++++++++++++++++-- .../payments/payment-allocation.service.ts | 29 +++++++++- backend/src/types/vendure-extensions.d.ts | 20 +++---- 3 files changed, 89 insertions(+), 18 deletions(-) diff --git a/backend/spec/services/payments/payment-allocation.service.spec.ts b/backend/spec/services/payments/payment-allocation.service.spec.ts index 61db95d5..80701932 100644 --- a/backend/spec/services/payments/payment-allocation.service.spec.ts +++ b/backend/spec/services/payments/payment-allocation.service.spec.ts @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, jest } from '@jest/globals'; import { Order, OrderService, Payment, PaymentService, RequestContext, User } from '@vendure/core'; +import { In } from 'typeorm'; import { PaymentAllocationService } from '../../../src/services/payments/payment-allocation.service'; import { FinancialService } from '../../../src/services/financial/financial.service'; import { OpenSessionService } from '../../../src/services/financial/open-session.service'; @@ -251,8 +252,10 @@ describe('PaymentAllocationService', () => { code: 'ORD-001', state: 'ArrangingPayment', customer: { id: 'cust-1' }, - customFields: { cashierPendingAt: new Date('2024-02-15T10:30:00Z') }, - createdByUserId: createdByUser, + customFields: { + cashierPendingAt: new Date('2024-02-15T10:30:00Z'), + createdByUserId: createdByUser, + }, } as unknown as Order; const orderRepo: Record = { @@ -278,7 +281,53 @@ describe('PaymentAllocationService', () => { expect(result[0].createdBy).toBe(createdByUser); expect(orderRepo.find).toHaveBeenCalledWith( expect.objectContaining({ - relations: ['customer', 'createdByUserId'], + relations: ['customer'], + }) + ); + }); + + it('resolves createdBy from a scalar user ID', async () => { + const createdByUser = { id: 'user-2', identifier: 'salesperson@example.com' } as User; + const pendingOrder = { + id: 'order-2', + code: 'ORD-002', + state: 'ArrangingPayment', + customer: { id: 'cust-2' }, + customFields: { + cashierPendingAt: new Date('2024-02-15T10:30:00Z'), + createdByUserId: createdByUser.id, + }, + } as unknown as Order; + + const orderRepo: Record = { + update: jest.fn(), + findOne: jest.fn(), + find: jest.fn(), + }; + orderRepo.find.mockReturnValue(Promise.resolve([pendingOrder])); + + const userRepo = { + find: jest.fn().mockReturnValue(Promise.resolve([createdByUser])), + }; + + mockConnection.getRepository.mockImplementation((_: any, entity: any) => + entity === Order ? orderRepo : userRepo + ); + + mockFinancialService.getOrderPaymentStatus.mockResolvedValue({ + totalOwed: 10000, + amountPaid: 0, + amountOwing: 10000, + }); + + const result = await service.getPendingCashierOrders(ctx); + + expect(result).toHaveLength(1); + expect(result[0].order.id).toBe('order-2'); + expect(result[0].createdBy).toBe(createdByUser); + expect(userRepo.find).toHaveBeenCalledWith( + expect.objectContaining({ + where: { id: In([createdByUser.id]) }, }) ); }); @@ -289,8 +338,7 @@ describe('PaymentAllocationService', () => { code: 'ORD-001', state: 'ArrangingPayment', customer: { id: 'cust-1' }, - customFields: { cashierPendingAt: new Date() }, - createdByUserId: null, + customFields: { cashierPendingAt: new Date(), createdByUserId: null }, } as unknown as Order; const orderRepo: Record = { diff --git a/backend/src/services/payments/payment-allocation.service.ts b/backend/src/services/payments/payment-allocation.service.ts index f0722ba8..5a5e2d2f 100644 --- a/backend/src/services/payments/payment-allocation.service.ts +++ b/backend/src/services/payments/payment-allocation.service.ts @@ -767,10 +767,28 @@ export class PaymentAllocationService { state: In(PAYABLE_ORDER_STATES), customFields: { cashierPendingAt: Not(IsNull()) } as any, }, - relations: ['customer', 'createdByUserId'], + relations: ['customer'], order: { createdAt: 'ASC' }, // Oldest first }); + // Relation custom fields store the FK ID when the relation is not loaded, + // and the User entity when loaded via relations/eager loading. Batch-resolve + // the creator IDs to User entities to avoid N+1 queries. + const creatorIds = new Set(); + for (const order of orders) { + const ref = order.customFields?.createdByUserId; + if (ref != null && typeof ref !== 'object') { + creatorIds.add(ref); + } + } + const users = + creatorIds.size > 0 + ? await this.connection.getRepository(ctx, User).find({ + where: { id: In([...creatorIds]) }, + }) + : []; + const userById = new Map(users.map(u => [u.id.toString(), u])); + // Channel isolation + correctness: getOrderPaymentStatus reads AR by orderId within the // request's channel, so an order parked in another channel resolves to amountOwing 0 here // and is excluded. This also drops any order whose receivable was reversed/voided while the @@ -780,11 +798,18 @@ export class PaymentAllocationService { const status = await this.financialService.getOrderPaymentStatus(ctx, order.id.toString()); if (status.amountOwing > 0) { const customFields = order.customFields ?? {}; + const createdByRef = customFields.createdByUserId; + const createdBy: User | null = + createdByRef == null + ? null + : typeof createdByRef === 'object' + ? createdByRef + : (userById.get(createdByRef.toString()) ?? null); pending.push({ order, amountOwing: status.amountOwing, pendingSince: customFields.cashierPendingAt ?? null, - createdBy: order.createdByUserId ?? null, + createdBy, }); } } diff --git a/backend/src/types/vendure-extensions.d.ts b/backend/src/types/vendure-extensions.d.ts index 85537be7..8f0f2341 100644 --- a/backend/src/types/vendure-extensions.d.ts +++ b/backend/src/types/vendure-extensions.d.ts @@ -1,22 +1,20 @@ -import { User } from '@vendure/core'; +import { ID, User } from '@vendure/core'; declare module '@vendure/core' { /** - * Scalar custom fields defined in backend/src/config/custom-fields/entity.custom-fields.ts. + * Custom fields defined in backend/src/config/custom-fields/entity.custom-fields.ts. * Augmenting the core class lets the rest of the backend access these fields without * resorting to type assertions. + * + * Relation custom fields hold the related entity when eager-/relation-loaded, + * or the foreign-key ID when loaded via customFields only. Typing them as + * `User | ID | null` keeps both states type-safe. */ interface CustomOrderFields { + createdByUserId?: User | ID | null; + lastModifiedByUserId?: User | ID | null; + reversedByUserId?: User | ID | null; cashierPendingAt?: Date | null; reversedAt?: Date | null; } - - /** - * Relation custom fields defined in backend/src/config/custom-fields/entity.custom-fields.ts. - * Vendure stores relation custom fields as properties on the entity itself. - */ - interface Order { - createdByUserId?: User | null; - lastModifiedByUserId?: User | null; - } }