89 lines
3.0 KiB
TypeScript
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 });
|
|
});
|
|
}
|
|
}
|