manicanldes-backend/src/checkout/checkout.store.ts

89 lines
3.0 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { AccessStore } from '../identity/access.store';
import type { Principal } from '../identity/identity.types';
import { recordAudit } from '../identity/audit';
import { AppError } from '../common/errors/app-error';
import { assertReplay, commandHash } from '../inventory/inventory.policy';
import type { CheckoutInput } from './checkout.schemas';
import { checkoutSnapshot } from './checkout-snapshot';
import { holdOrderStock } from './stock-allocation';
import { orderView } from './order-view';
import { snapshotPrice } from '../pricing/snapshot-price';
import { enqueueEvent } from '../events/enqueue-event';
@Injectable()
export class CheckoutStore {
constructor(private readonly access: AccessStore) {}
create(actor: Principal, input: CheckoutInput) {
const requestHash = commandHash(
input.cartVersion,
input.addressId,
input.couponCode ?? '',
);
// The organization lock also serializes catalog, coupon and cart changes.
// Stock locks below coordinate with independent inventory transactions.
return this.access.mutate(actor, null, async (tx) => {
const previous = await tx.order.findUnique({
where: {
userId_organizationId_idempotencyKey: {
userId: actor.userId,
organizationId: actor.organizationId,
idempotencyKey: input.idempotencyKey,
},
},
include: { lines: true, pricing: true },
});
if (previous) {
assertReplay(previous.requestHash, requestHash);
return orderView(previous);
}
const now = new Date();
if (
(await tx.order.count({
where: {
userId: actor.userId,
organizationId: actor.organizationId,
status: 'PENDING_PAYMENT',
expiresAt: { gt: now },
},
})) >= 10
)
throw new AppError('ORDER_LIMIT');
const { cartId, lines, ...snapshot } = await checkoutSnapshot(
tx,
actor,
input,
);
const expiresAt = new Date(now.getTime() + 15 * 60000);
const order = await tx.order.create({
data: {
...snapshot,
organizationId: actor.organizationId,
userId: actor.userId,
idempotencyKey: input.idempotencyKey,
requestHash,
expiresAt,
lines: { create: lines },
},
include: { lines: true },
});
await holdOrderStock(tx, actor, order.id, expiresAt, lines);
const pricing = await snapshotPrice(tx, order);
await enqueueEvent(tx, actor.organizationId, order.id, 'order.created');
await tx.cartLine.deleteMany({ where: { cartId } });
await tx.cart.update({
where: { id: cartId },
data: { version: { increment: 1 } },
});
await recordAudit(
tx,
actor.organizationId,
actor.userId,
'order.created',
order.id,
);
return orderView({ ...order, pricing });
});
}
}