import { identityApp, type IdentityApp } from './helpers/identity-app'; import { checkoutFixture } from './helpers/checkout'; import { secondActor } from './helpers/commerce'; describe('order ownership and cancellation', () => { let ctx: IdentityApp; beforeAll(async () => { ctx = await identityApp(); }, 60000); afterAll(async () => { await ctx.close(); }); beforeEach(async () => { await ctx.clearLimits(); }); it('isolates customers and organizations while allowing scoped staff reads', async () => { const f = await checkoutFixture(ctx); const order = await ctx .api() .post('/api/v1/checkout') .auth(f.actor.token, { type: 'bearer' }) .send(f.input) .expect(201); const other = await secondActor(ctx); const foreign = await secondActor(ctx, false, [ 'orders.read', 'orders.manage', ]); for (const actor of [other, foreign]) { await ctx .api() .get('/api/v1/orders/' + order.body.id) .auth(actor.token, { type: 'bearer' }) .expect(404); await ctx .api() .post('/api/v1/orders/' + order.body.id + '/cancel') .auth(actor.token, { type: 'bearer' }) .expect(404); } await ctx .api() .get('/api/v1/admin/orders/' + order.body.id) .auth(other.token, { type: 'bearer' }) .expect(403); await ctx .api() .get('/api/v1/admin/orders/' + order.body.id) .auth(foreign.token, { type: 'bearer' }) .expect(404); await ctx .api() .post('/api/v1/admin/orders/' + order.body.id + '/cancel') .auth(foreign.token, { type: 'bearer' }) .expect(404); await ctx .api() .get('/api/v1/admin/orders/' + order.body.id) .auth(ctx.token, { type: 'bearer' }) .expect(200); const own = await ctx .api() .get('/api/v1/orders') .auth(f.actor.token, { type: 'bearer' }) .expect(200); expect(own.body.map((row: { id: string }) => row.id)).toEqual([ order.body.id, ]); const others = await ctx .api() .get('/api/v1/orders') .auth(other.token, { type: 'bearer' }) .expect(200); expect(others.body).toEqual([]); const staff = await ctx .api() .get('/api/v1/admin/orders?limit=1') .auth(ctx.token, { type: 'bearer' }) .expect(200); expect(staff.body).toHaveLength(1); expect(staff.body[0].address).toBeUndefined(); }); it('cancels once, releases holds and blocks standalone reservation transitions', async () => { const f = await checkoutFixture(ctx); const order = await ctx .api() .post('/api/v1/checkout') .auth(f.actor.token, { type: 'bearer' }) .send(f.input) .expect(201); const hold = await ctx.db.stockReservation.findFirstOrThrow({ where: { orderId: order.body.id }, }); const denied = await ctx .api() .post('/api/v1/inventory/reservations/' + hold.id + '/commit') .auth(ctx.token, { type: 'bearer' }) .expect(409); expect(denied.body.code).toBe('ORDER_RESERVATION_MANAGED'); const result = await ctx .api() .post('/api/v1/admin/orders/' + order.body.id + '/cancel') .auth(ctx.token, { type: 'bearer' }) .expect(201); expect(result.body.status).toBe('CANCELLED'); await ctx .api() .post('/api/v1/orders/' + order.body.id + '/cancel') .auth(f.actor.token, { type: 'bearer' }) .expect(201); expect( ( await ctx.db.stockReservation.findUniqueOrThrow({ where: { id: hold.id }, }) ).status, ).toBe('RELEASED'); expect( await ctx.db.auditEvent.count({ where: { targetId: order.body.id, action: 'order.cancelled' }, }), ).toBe(1); expect( (await ctx.db.stockItem.findUniqueOrThrow({ where: { id: f.stock.id } })) .onHand, ).toBe(10); }); it('enforces immutable snapshots and line arithmetic through SQL', async () => { const f = await checkoutFixture(ctx); const order = await ctx .api() .post('/api/v1/checkout') .auth(f.actor.token, { type: 'bearer' }) .send(f.input) .expect(201); await expect( ctx.executeSql( `UPDATE orders SET subtotal = 1 WHERE id = '${order.body.id}'`, ), ).rejects.toThrow(); await expect( ctx.executeSql(`DELETE FROM orders WHERE id = '${order.body.id}'`), ).rejects.toThrow(); await expect( ctx.executeSql( `UPDATE order_lines SET quantity = 10 WHERE order_id = '${order.body.id}'`, ), ).rejects.toThrow(); await expect( ctx.executeSql( `DELETE FROM order_lines WHERE order_id = '${order.body.id}'`, ), ).rejects.toThrow(); await expect( ctx.executeSql( `UPDATE stock_reservations SET order_id = NULL WHERE order_id = '${order.body.id}'`, ), ).rejects.toThrow(); await expect( ctx.executeSql( `DELETE FROM stock_reservations WHERE order_id = '${order.body.id}'`, ), ).rejects.toThrow(); }); it('rejects a later line insertion that would change a committed order snapshot', async () => { const f = await checkoutFixture(ctx); const result = await ctx .api() .post('/api/v1/checkout') .auth(f.actor.token, { type: 'bearer' }) .send(f.input) .expect(201); const variant = await ctx.db.productVariant.create({ data: { organizationId: f.actor.organizationId, productId: f.product.id, sku: result.body.id, name: 'Extra line', price: '1.00', }, }); await expect( ctx.executeSql(`INSERT INTO order_lines (id, order_id, organization_id, variant_id, sku, product_name, variant_name, quantity, unit_price, line_total) VALUES (gen_random_uuid(), '${result.body.id}', '${f.actor.organizationId}', '${variant.id}', 'EXTRA', 'Extra', 'Extra', 1, 1, 1)`), ).rejects.toThrow('Order subtotal does not match lines'); expect( await ctx.db.orderLine.count({ where: { orderId: result.body.id } }), ).toBe(1); }); });