import { CustomersService } from './customers.service'; /** * `GET customer/me/orders` must carry each item's product and variant. * * The apps used to label order rows by downloading the ENTIRE catalogue and * joining it in the browser (composables/useOrderItemProduct.ts). With the real * catalogue at ~16k products that is the wrong trade for a few row labels, and * it was also broken for the case order history exists for: a product that has * since left the storefront is absent from the catalogue, so the row rendered * blank. * * Both apps now read these fields straight off the item, so the include is part * of the contract — hence a test, which this method did not have. */ describe('CustomersService.getOrders — items carry their product', () => { function setup() { const findMany = jest.fn().mockResolvedValue([]); const count = jest.fn().mockResolvedValue(0); const prisma = { order: { findMany, count } }; // getOrders touches neither the accounting sync nor anything on it. const service = new CustomersService(prisma as never, {} as never); return { service, findMany, count }; } it('includes the variant, its product and the first image', async () => { const { service, findMany } = setup(); await service.getOrders('c1'); const include = findMany.mock.calls[0][0].include; const variant = include.items.include.productVariant; expect(variant.select.title).toBe(true); expect(variant.select.product.select.title).toBe(true); // images[0] is the product's main photo, so it must be ordered and limited // to one — the apps read `images[0].url` and nothing else. expect(variant.select.product.select.images).toMatchObject({ orderBy: { sortOrder: 'asc' }, take: 1, }); }); it('includes status events oldest-first for the detail timeline', async () => { const { service, findMany } = setup(); await service.getOrders('c1'); expect(findMany.mock.calls[0][0].include.statusEvents).toMatchObject({ orderBy: { createdAt: 'asc' }, select: { id: true, fromStatus: true, toStatus: true, note: true, createdAt: true, }, }); }); it('is scoped to the caller and newest first', async () => { // Guards the two properties that were already here: one customer must never // receive another's orders, and the list is reverse-chronological. const { service, findMany } = setup(); await service.getOrders('c1'); expect(findMany.mock.calls[0][0]).toMatchObject({ where: { customerId: 'c1' }, orderBy: { createdAt: 'desc' }, }); }); it('does not filter the product by storefront visibility', async () => { // Deliberate: an order for a product that has since been deactivated, or // had its photo removed, must still show what was bought. The storefront // rules apply to the catalogue, not to history. const { service, findMany } = setup(); await service.getOrders('c1'); const product = findMany.mock.calls[0][0].include.items.include.productVariant.select.product; expect(product.where).toBeUndefined(); }); it('pages with skip/take and returns count', async () => { const { service, findMany, count } = setup(); findMany.mockResolvedValue([{ id: 'o1' }]); count.mockResolvedValue(41); await expect(service.getOrders('c1', { page: 2, limit: 20 })).resolves.toEqual({ result: [{ id: 'o1' }], count: 41, page: 2, limit: 20, }); expect(findMany.mock.calls[0][0]).toMatchObject({ skip: 20, take: 20 }); }); it('filters by status and orderNumber on the server', async () => { const { service, findMany } = setup(); await service.getOrders('c1', { status: 'PLACED', q: '12' }); expect(findMany.mock.calls[0][0].where).toEqual({ customerId: 'c1', status: 'PLACED', orderNumber: { contains: '12', mode: 'insensitive' }, }); }); it('ignores an unknown status instead of throwing', async () => { const { service, findMany } = setup(); await service.getOrders('c1', { status: 'NOT_A_STATUS' }); expect(findMany.mock.calls[0][0].where).toEqual({ customerId: 'c1' }); }); });