Bariki cashback, order edit, and current work in progress
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
dda917fc71
commit
45477fb018
94 files changed
+10955
-650
No files matched your search
@@ -25,7 +25,9 @@ import { getServiceByCode, type ParsedService } from './catalog.ts';
|
||||
import { getMembership } from './clients.ts';
|
||||
import { DomainError, type Ctx, type DomainEvent } from './context.ts';
|
||||
import { getSettings } from './settings.ts';
|
||||
import { recordPartnerEarning } from './servicePartners.ts';
|
||||
import { findPromo } from './promo.ts';
|
||||
import { accrueForOrder, onOrderCancelled, redeemPoints, takePointsEvents } from './points.ts';
|
||||
|
||||
export async function quoteForClient(ctx: Ctx, clientId: string, serviceCode: string, rawParams: Record<string, unknown>, promoCode?: string | null) {
|
||||
const service = await getServiceByCode(ctx.db, serviceCode, { activeOnly: true });
|
||||
@@ -85,6 +87,8 @@ export interface CreateOrderInput {
|
||||
allowBusySlot?: boolean;
|
||||
/** промокод на скидку (применяется, если выгоднее скидки подписки) */
|
||||
promoCode?: string | null;
|
||||
/** оплатить часть бариками: сумма (не больше N% и баланса); с промокодом нельзя */
|
||||
usePoints?: number;
|
||||
}
|
||||
|
||||
/** ORD-5: подтверждение заказа → «Новый», чек-лист из шаблона, запись в историю */
|
||||
@@ -94,6 +98,8 @@ export async function createOrder(ctx: Ctx, input: CreateOrderInput) {
|
||||
const { service, params, quote: q, promo } = await quoteForClient(ctx, input.clientId, input.serviceCode, input.params, input.promoCode);
|
||||
if (!service.isOnline && (!input.addressText?.trim() || !input.districtId)) throw new DomainError('ADDRESS_REQUIRED');
|
||||
const flow = getFlow(service.flow);
|
||||
if (input.usePoints && promo) throw new DomainError('POINTS_WITH_PROMO');
|
||||
const pointEvents: DomainEvent[] = [];
|
||||
|
||||
const order = await ctx.db.$transaction(
|
||||
async (tx) => {
|
||||
@@ -134,11 +140,17 @@ export async function createOrder(ctx: Ctx, input: CreateOrderInput) {
|
||||
await tx.promoCodeUse.create({ data: { promoId: promo.id, clientId: input.clientId, orderId: created.id } });
|
||||
await tx.promoCode.update({ where: { id: promo.id }, data: { usedCount: { increment: 1 } } });
|
||||
}
|
||||
if (input.usePoints && input.usePoints > 0) {
|
||||
await redeemPoints(ctx, { clientId: input.clientId, amount: input.usePoints, orderId: created.id, orderRef: `#${created.number}`, orderAmount: q.totalPrice, channel: input.createdBy ? 'WEB' : 'CLIENT' }, { adminId: input.createdBy?.id ?? null, owner: true }, tx);
|
||||
pointEvents.push(...takePointsEvents(tx));
|
||||
return tx.order.update({ where: { id: created.id }, data: { pointsUsed: input.usePoints } });
|
||||
}
|
||||
return created;
|
||||
},
|
||||
{ isolationLevel: 'ReadCommitted' },
|
||||
);
|
||||
await ctx.notify.emit({ type: 'order.created', orderId: order.id });
|
||||
for (const e of pointEvents) await ctx.notify.emit(e);
|
||||
return order;
|
||||
}
|
||||
|
||||
@@ -150,7 +162,7 @@ export async function guardContext(db: Db | Tx, orderId: string): Promise<GuardC
|
||||
db.lessonPackage.findUnique({ where: { orderId } }),
|
||||
]);
|
||||
return {
|
||||
checklistComplete: items.every((i) => i.checkedAt && (!i.requiresPhoto || i.fileId)),
|
||||
checklistComplete: items.every((i) => i.checkedAt && (!i.requiresPhoto || i.fileId || i.photoWaivedReason)),
|
||||
estimateAccepted: estimate?.status === 'ACCEPTED',
|
||||
lessonsTotal: lessons?.total,
|
||||
lessonsCompleted: lessons?.completed,
|
||||
@@ -211,12 +223,16 @@ export async function transitionOrder(ctx: Ctx, input: TransitionInput, txArg?:
|
||||
comment: input.comment,
|
||||
},
|
||||
});
|
||||
return { order: updated, from: order.status };
|
||||
// барики: отмена возвращает списанные и отменяет начисление; выполненный и оплаченный заказ — кэшбек
|
||||
if (input.to === 'CANCELLED') await onOrderCancelled(ctx, tx, order.id);
|
||||
else if (flow.done.includes(input.to)) await accrueForOrder(ctx, tx, order.id);
|
||||
return { order: updated, from: order.status, extra: takePointsEvents(tx) };
|
||||
};
|
||||
const result = txArg ? await run(txArg) : await ctx.db.$transaction(run);
|
||||
const event: DomainEvent = { type: 'order.status', orderId: input.orderId, from: result.from, to: input.to };
|
||||
if (txArg) return { ...result, event };
|
||||
await ctx.notify.emit(event);
|
||||
for (const e of result.extra) await ctx.notify.emit(e);
|
||||
return { ...result, event };
|
||||
}
|
||||
|
||||
@@ -304,6 +320,7 @@ export async function cancelByClient(ctx: Ctx, input: { orderId: string; clientI
|
||||
return r;
|
||||
});
|
||||
await ctx.notify.emit(result.event);
|
||||
for (const e of result.extra) await ctx.notify.emit(e);
|
||||
return { order: result.order, fee: policy.fee };
|
||||
}
|
||||
|
||||
@@ -426,18 +443,19 @@ export async function repeatDraft(db: Db, orderId: string, clientId: string) {
|
||||
export async function markPaid(ctx: Ctx, input: { orderId: string; by: { type: 'ADMIN' | 'PROVIDER'; id: string }; paymentMethodId?: string }) {
|
||||
const provider = new ManualPaymentProvider();
|
||||
const settings = await getSettings(ctx.db);
|
||||
return ctx.db.$transaction(async (tx) => {
|
||||
const events: DomainEvent[] = [];
|
||||
const res = await ctx.db.$transaction(async (tx) => {
|
||||
const order = await tx.order.findUnique({ where: { id: input.orderId }, include: { client: { include: { agency: true } } } });
|
||||
if (!order) throw new DomainError('NOT_FOUND');
|
||||
if (input.by.type === 'PROVIDER' && order.providerId !== input.by.id) throw new DomainError('NOT_FOUND');
|
||||
if (order.paymentStatus === 'PAID' || order.status === 'CANCELLED') throw new DomainError('NOT_PAID_STATE');
|
||||
const res = await provider.confirmPayment(
|
||||
{ orderId: order.id, amount: order.totalPrice, currency: 'AMD', description: `#${order.number}` },
|
||||
{ orderId: order.id, amount: order.totalPrice - order.pointsUsed, currency: 'AMD', description: `#${order.number}` },
|
||||
{ markedBy: input.by.id },
|
||||
);
|
||||
const now = ctx.now();
|
||||
await tx.payment.create({
|
||||
data: { orderId: order.id, provider: res.provider, amount: order.totalPrice, status: 'PAID', markedById: input.by.id },
|
||||
data: { orderId: order.id, provider: res.provider, amount: order.totalPrice - order.pointsUsed, status: 'PAID', markedById: input.by.id },
|
||||
});
|
||||
const updated = await tx.order.update({
|
||||
where: { id: order.id },
|
||||
@@ -450,13 +468,20 @@ export async function markPaid(ctx: Ctx, input: { orderId: string; by: { type: '
|
||||
agencyId: agency.id,
|
||||
orderId: order.id,
|
||||
pct: agency.commissionPct,
|
||||
amount: agencyCommission(order.totalPrice, agency.commissionPct),
|
||||
amount: agencyCommission(order.totalPrice - order.pointsUsed, agency.commissionPct),
|
||||
period: yerevanMonthKey(now),
|
||||
},
|
||||
});
|
||||
}
|
||||
// заказ выполнил сотрудник партнёра — фиксируем доход партнёра по его условиям
|
||||
await recordPartnerEarning(tx, order, settings, now);
|
||||
// уже выполнен — сразу кэшбек
|
||||
await accrueForOrder(ctx, tx, order.id);
|
||||
events.push(...takePointsEvents(tx));
|
||||
return updated;
|
||||
});
|
||||
for (const e of events) await ctx.notify.emit(e);
|
||||
return res;
|
||||
}
|
||||
|
||||
export function isOrderActive(flowCode: Parameters<typeof isTerminal>[0], status: OrderStatus) {
|
||||
|
||||
Reference in new issue
Block a user