manicanldes-backend/test/checkout-rollback.spec.ts

108 lines
3.3 KiB
TypeScript

import { randomUUID } from 'node:crypto';
import { identityApp, type IdentityApp } from './helpers/identity-app';
import { checkoutFixture } from './helpers/checkout';
import * as audit from '../src/identity/audit';
describe('checkout allocation and final-write rollback', () => {
let ctx: IdentityApp;
beforeAll(async () => {
ctx = await identityApp();
}, 60000);
afterAll(async () => {
await ctx.close();
});
beforeEach(async () => {
await ctx.clearLimits();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('rolls back order, holds and cart clearing when the audit write fails', async () => {
const f = await checkoutFixture(ctx);
jest
.spyOn(audit, 'recordAudit')
.mockRejectedValueOnce(new Error('Synthetic audit failure'));
const result = await ctx
.api()
.post('/api/v1/checkout')
.auth(f.actor.token, { type: 'bearer' })
.send(f.input)
.expect(500);
expect(result.body.code).toBe('INTERNAL_FAILURE');
expect(JSON.stringify(result.body)).not.toContain('Synthetic');
expect(
await ctx.db.order.count({ where: { userId: f.actor.userId } }),
).toBe(0);
expect(
await ctx.db.stockReservation.count({
where: { userId: f.actor.userId },
}),
).toBe(0);
const cart = await ctx
.api()
.get('/api/v1/cart')
.auth(f.actor.token, { type: 'bearer' })
.expect(200);
expect(cart.body.version).toBe(1);
expect(cart.body.lines).toHaveLength(1);
await ctx
.api()
.post('/api/v1/checkout')
.auth(f.actor.token, { type: 'bearer' })
.send(f.input)
.expect(201);
});
it('splits a line across warehouses without consuming on-hand stock', async () => {
const f = await checkoutFixture(ctx, 1);
const warehouse = await ctx.db.warehouse.create({
data: { organizationId: f.actor.organizationId, name: randomUUID() },
});
const stock = await ctx.db.stockItem.create({
data: {
organizationId: f.actor.organizationId,
warehouseId: warehouse.id,
variantId: f.variant.id,
},
});
await ctx
.api()
.post('/api/v1/inventory/adjustments')
.auth(ctx.token, { type: 'bearer' })
.send({
stockItemId: stock.id,
delta: 1,
reason: 'Opening balance',
idempotencyKey: randomUUID(),
})
.expect(201);
const result = await ctx
.api()
.post('/api/v1/checkout')
.auth(f.actor.token, { type: 'bearer' })
.send(f.input)
.expect(201);
const holds = await ctx.db.stockReservation.findMany({
where: { orderId: result.body.id },
});
expect(holds).toHaveLength(2);
expect(holds.map((hold) => hold.quantity)).toEqual([1, 1]);
});
it('revalidates product availability and currency when checking out a saved cart', async () => {
const f = await checkoutFixture(ctx);
await ctx.db.productVariant.update({
where: { id: f.variant.id },
data: { active: false },
});
const response = await ctx
.api()
.post('/api/v1/checkout')
.auth(f.actor.token, { type: 'bearer' })
.send(f.input)
.expect(409);
expect(response.body.code).toBe('CART_ITEM_UNAVAILABLE');
expect(
await ctx.db.order.count({ where: { userId: f.actor.userId } }),
).toBe(0);
});
});