Legacy fixed `orders.reagent_id` once, when the order was placed, and its «میزان فروش» summed `customer_id = X OR reagent_id = X`. The app credited orders to the customer's CURRENT referrer and left out an expert's own orders, so 9 of 49 imported experts showed a different figure. - Order.salesAgentId (+ referrerCustomerId for 17 legacy orders whose referrer was not an expert); additive migration, recent app orders backfilled from the customer's referrer - createOrder records the expert at placement time - one shared filter (agent-orders.ts) for the experts list, expert detail, dashboard and the expert app; child orders follow salesAgentId - customer page gets its «میزان فروش» back - import carries orders.reagent_id as is; ex-experts kept as admins keep their profile so their own orders stay linked - reconcile-sales: 1,557 of 1,558 match legacy's own formula; balances unchanged Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
430 lines
19 KiB
TypeScript
430 lines
19 KiB
TypeScript
/**
|
||
* Orders, the ledger, favourites and custom orders.
|
||
* Rules: docs/DATA-MIGRATION-MAP.md §7, §8, §10, §10.7.
|
||
*/
|
||
import {
|
||
LedgerCurrency,
|
||
LedgerDirection,
|
||
LedgerSourceType,
|
||
OrderChannel,
|
||
OrderStatus,
|
||
PreOrderStatus,
|
||
Prisma,
|
||
} from '@prisma/client';
|
||
import { upsert, type Ctx } from '../lib/context';
|
||
import { grams, int, jalaliDate, percent, rial, tehran, text, toNumber } from '../lib/normalize';
|
||
|
||
type Row = Record<string, unknown>;
|
||
const D = Prisma.Decimal;
|
||
|
||
/** Order::$STS (Order.php:66-74). 7 «کنسل شده» is commented out in legacy and absent from the data. */
|
||
const ORDER_STATUS: Record<number, OrderStatus> = {
|
||
0: OrderStatus.SUSPENDED,
|
||
1: OrderStatus.PLACED,
|
||
2: OrderStatus.ADMIN_APPROVED,
|
||
3: OrderStatus.SOURCING,
|
||
4: OrderStatus.PREPARING,
|
||
5: OrderStatus.DELIVERED_TO_AGENT,
|
||
6: OrderStatus.DELIVERED_TO_CUSTOMER,
|
||
};
|
||
|
||
/* ───────────── orders ───────────── */
|
||
|
||
export async function orders(ctx: Ctx) {
|
||
const S = 'orders';
|
||
// Wholesale only. Retail `orders_orders` is deferred by the owner (§7.2) and
|
||
// must not be mapped with this status table: its 7 means «تحویل مشتری».
|
||
const rows = await ctx.q<Row>('SELECT * FROM orders WHERE deleted_at IS NULL');
|
||
for (const r of rows) {
|
||
const customerId = ctx.map.get('customer', r.customer_id);
|
||
if (!customerId) {
|
||
ctx.report.count(S, 'skipped: customer not migrated');
|
||
ctx.report.note(S, `legacy order #${String(r.id)} skipped: its customer #${String(r.customer_id)} was not migrated (status ${String(r.sts)})`);
|
||
continue;
|
||
}
|
||
const sts = int(r.sts);
|
||
const status = sts !== null ? ORDER_STATUS[sts] : undefined;
|
||
if (!status) {
|
||
ctx.report.count(S, 'skipped: unknown status');
|
||
ctx.report.note(S, `legacy order #${String(r.id)} has status ${String(r.sts)}, which has no mapping`);
|
||
continue;
|
||
}
|
||
const orderNumber = `H-${String(r.id)}`;
|
||
// `orders.reagent_id` is the expert the order was credited to when it was
|
||
// placed (never moved afterwards) — carried as is, NOT re-derived from the
|
||
// customer's current referrer (owner ruling 2026-10-08). It names a
|
||
// Customer row in legacy: an expert maps to their AdminUser; anyone else
|
||
// (one customer, 17 orders from 2022) is kept as the referrer customer.
|
||
let salesAgentId: string | null = null;
|
||
let referrerCustomerId: string | null = null;
|
||
if (r.reagent_id !== null) {
|
||
salesAgentId = ctx.map.get('expert', r.reagent_id) ?? null;
|
||
if (!salesAgentId) referrerCustomerId = ctx.map.get('customer', r.reagent_id) ?? null;
|
||
if (!salesAgentId && !referrerCustomerId) {
|
||
ctx.report.count(S, 'placing expert not migrated — left empty');
|
||
ctx.report.note(S, `legacy order #${String(r.id)}: its reagent #${String(r.reagent_id)} was not migrated`);
|
||
} else {
|
||
ctx.report.count(S, salesAgentId ? 'credited to its legacy expert' : 'credited to its legacy referrer customer');
|
||
}
|
||
}
|
||
const data = {
|
||
salesAgentId,
|
||
referrerCustomerId,
|
||
customerId,
|
||
channel: OrderChannel.WHOLESALE,
|
||
status,
|
||
// `input` is what legacy debits (CustomerController::updateRemain) and
|
||
// equals `w` on all 300 live orders; `wage` is the making-charge part of it.
|
||
totalGoldWeightGrams: grams(r.input),
|
||
wageWeightGrams: grams(r.wage),
|
||
flatRialCharge: rial(r.rial_input),
|
||
// Legacy 0 «معلق» is the over-credit-limit hold.
|
||
isSuspendedOverCreditLimit: sts === 0,
|
||
// Legacy records no delivery time; the last change of a delivered order is the best available.
|
||
deliveredAt: sts === 6 ? tehran(r.updated_at) : null,
|
||
};
|
||
const res = await upsert(ctx, 'order', r.id, {
|
||
exists: async (id) => !!(await ctx.db.order.findUnique({ where: { id } })),
|
||
find: () => ctx.db.order.findUnique({ where: { orderNumber }, select: { id: true } }),
|
||
create: () => ctx.db.order.create({ data: { ...data, orderNumber, createdAt: tehran(r.created_at) ?? undefined } }),
|
||
update: async (id, how) => {
|
||
if (how === 'created') await ctx.db.order.update({ where: { id }, data });
|
||
},
|
||
});
|
||
ctx.report.count(S, `${res.isNew ? 'created' : 'updated'} — ${status}`); }
|
||
await orderItems(ctx);
|
||
}
|
||
|
||
async function orderItems(ctx: Ctx) {
|
||
const S = 'order lines';
|
||
const rows = await ctx.q<Row>(
|
||
`SELECT oi.* FROM order_items oi JOIN orders o ON o.id = oi.order_id AND o.deleted_at IS NULL
|
||
WHERE oi.deleted_at IS NULL`,
|
||
);
|
||
for (const r of rows) {
|
||
const orderId = ctx.map.get('order', r.order_id);
|
||
if (!orderId) continue;
|
||
const productVariantId = ctx.map.get('variant', r.variant_id);
|
||
if (!productVariantId) {
|
||
ctx.report.count(S, 'skipped: variant not migrated');
|
||
ctx.report.note(S, `legacy order line #${String(r.id)} (order #${String(r.order_id)}) lost: variant #${String(r.variant_id)} not found`);
|
||
continue;
|
||
}
|
||
const data = {
|
||
orderId,
|
||
productVariantId,
|
||
quantity: int(r.qty) ?? 1,
|
||
rashWeightGramsSnapshot: grams(r.rash_weight),
|
||
wagePercentSnapshot: percent(r.percent),
|
||
discountPercentSnapshot: percent(r.discount),
|
||
computedWeightGrams: grams(r.w),
|
||
flatRialCharge: rial(r.rial_pay),
|
||
};
|
||
const res = await upsert(ctx, 'orderitem', r.id, {
|
||
exists: async (id) => !!(await ctx.db.orderItem.findUnique({ where: { id } })),
|
||
find: async () => null,
|
||
create: () => ctx.db.orderItem.create({ data }),
|
||
update: async (id) => {
|
||
await ctx.db.orderItem.update({ where: { id }, data });
|
||
},
|
||
});
|
||
ctx.report.count(S, res.isNew ? 'created' : 'updated');
|
||
// §7.4: the target keeps one snapshot; legacy's audit pair is lost.
|
||
if (toNumber(r.original_rash_weight) !== toNumber(r.rash_weight) || toNumber(r.original_percent) !== toNumber(r.percent)) {
|
||
ctx.report.count(S, 'weight or wage edited after ordering (original_* not carried)');
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ───────────── ledger ───────────── */
|
||
|
||
/** Cash::$TYPES (Major/Cash.php:56-62). Type 9 «سفارش» is handled from `orders`. */
|
||
const CASH_SOURCE: Record<number, LedgerSourceType> = {
|
||
1: LedgerSourceType.REPAYMENT_MELTED_GOLD,
|
||
2: LedgerSourceType.REPAYMENT_TRANSFER,
|
||
3: LedgerSourceType.REPAYMENT_CHEQUE,
|
||
4: LedgerSourceType.REPAYMENT_CASH,
|
||
};
|
||
|
||
/** Grams per mesghal. Legacy's price_per_gram is per mesghal (CashController.php:68). */
|
||
const MESGHAL = 4.331802;
|
||
|
||
/**
|
||
* Legacy debt is COMPUTED, never posted (CustomerController::updateRemain):
|
||
*
|
||
* remain = Σ orders.input (sts > 4) − Σ cashes.value (confirmed, type 1–4)
|
||
* rial_remain = Σ orders.rial_input (sts > 4)
|
||
*
|
||
* so the ledger is rebuilt from exactly those rows. Recomputing that formula on
|
||
* the 2026-10-04 dump reproduces the stored balance for 1,590 of 1,591
|
||
* customers to 0.0001 g (§10.7). Consequences:
|
||
* - `cashes` type 9 («سفارش») is NOT carried: it mirrors an order, and the
|
||
* order already posts that debt. Both would count it twice.
|
||
* - Rials paid (types 3/4 `price`) never reduce rial debt; a cash payment is
|
||
* converted to gold (`value`) and reduces the gold debt. Kept as is.
|
||
* - A balance the records cannot explain (customer #125: −9.8951 g, set by
|
||
* hand after its only document was deleted) is carried as an explicit
|
||
* ADJUSTMENT entry, so the customer sees the same number (owner, 2026-10-07).
|
||
*/
|
||
export async function ledger(ctx: Ctx) {
|
||
const S = 'ledger';
|
||
const entry = async (key: string, data: Prisma.LedgerEntryUncheckedCreateInput) => {
|
||
const res = await upsert(ctx, 'ledger', key, {
|
||
exists: async (id) => !!(await ctx.db.ledgerEntry.findUnique({ where: { id } })),
|
||
find: async () => null,
|
||
create: () => ctx.db.ledgerEntry.create({ data }),
|
||
update: async (id) => {
|
||
await ctx.db.ledgerEntry.update({ where: { id }, data });
|
||
},
|
||
});
|
||
return res.id;
|
||
};
|
||
const signed = (amount: number) => ({
|
||
direction: amount >= 0 ? LedgerDirection.DEBIT : LedgerDirection.CREDIT,
|
||
amount: grams(Math.abs(amount)),
|
||
});
|
||
|
||
// Orders delivered to the agent or the customer carry the debt.
|
||
const delivered = await ctx.q<Row>('SELECT * FROM orders WHERE deleted_at IS NULL AND sts > 4');
|
||
for (const o of delivered) {
|
||
const customerId = ctx.map.get('customer', o.customer_id);
|
||
const orderId = ctx.map.get('order', o.id);
|
||
if (!customerId || !orderId) continue;
|
||
const at = tehran(o.updated_at) ?? undefined;
|
||
if (toNumber(o.input) !== 0) {
|
||
await entry(`order:${String(o.id)}:gold`, {
|
||
customerId, orderId, ...signed(toNumber(o.input)),
|
||
currency: LedgerCurrency.GOLD_GRAM, sourceType: LedgerSourceType.ORDER_STOCK,
|
||
confirmed: true, confirmedAt: at, createdAt: at,
|
||
note: 'بدهی وزنی سفارش — انتقال از سیستم قدیم',
|
||
});
|
||
ctx.report.count(S, 'order debt (gold)');
|
||
}
|
||
if (toNumber(o.rial_input) !== 0) {
|
||
const amt = toNumber(o.rial_input);
|
||
await entry(`order:${String(o.id)}:rial`, {
|
||
customerId, orderId,
|
||
direction: amt >= 0 ? LedgerDirection.DEBIT : LedgerDirection.CREDIT,
|
||
amount: rial(Math.abs(amt)),
|
||
currency: LedgerCurrency.RIAL, sourceType: LedgerSourceType.ORDER_STOCK,
|
||
confirmed: true, confirmedAt: at, createdAt: at,
|
||
note: 'بدهی ریالی سفارش — انتقال از سیستم قدیم',
|
||
});
|
||
ctx.report.count(S, 'order debt (rial)');
|
||
}
|
||
}
|
||
|
||
const cashes = await ctx.q<Row>('SELECT * FROM cashes WHERE deleted_at IS NULL');
|
||
for (const c of cashes) {
|
||
const type = int(c.type);
|
||
if (type === 9) {
|
||
ctx.report.count(S, 'cash type 9 (order mirror) skipped — debt comes from the order');
|
||
continue;
|
||
}
|
||
const sourceType = type !== null ? CASH_SOURCE[type] : undefined;
|
||
if (!sourceType) {
|
||
ctx.report.count(S, 'skipped: unknown document type');
|
||
ctx.report.note(S, `legacy document #${String(c.id)} has type ${String(c.type)}`);
|
||
continue;
|
||
}
|
||
const customerId = ctx.map.get('customer', c.customer_id);
|
||
if (!customerId) {
|
||
ctx.report.count(S, 'skipped: customer not migrated');
|
||
continue;
|
||
}
|
||
// A repayment is a CREDIT; a negative `value` (4 transfers) is a debit.
|
||
const value = toNumber(c.value);
|
||
const received = jalaliDate(c.recive_date);
|
||
const confirmed = int(c.confirmed) === 1;
|
||
const ppg = toNumber(c.price_per_gram);
|
||
await entry(`cash:${String(c.id)}`, {
|
||
customerId,
|
||
currency: LedgerCurrency.GOLD_GRAM,
|
||
direction: value >= 0 ? LedgerDirection.CREDIT : LedgerDirection.DEBIT,
|
||
amount: grams(Math.abs(value)),
|
||
sourceType,
|
||
confirmed,
|
||
confirmedAt: confirmed ? tehran(c.updated_at) : null,
|
||
postedByAdminUserId: ctx.map.get('admin', c.user_id) ?? null,
|
||
karatPerMille: int(c.ayar),
|
||
labName: text(c.lab),
|
||
hallmark: text(c.ang),
|
||
accountName: text(c.account_name),
|
||
bankName: text(c.bank_name),
|
||
transferTo: text(c.to),
|
||
originalValue: c.original_value === null ? null : grams(c.original_value),
|
||
priceRial: c.price === null ? null : rial(c.price),
|
||
pricePerGramRialAtEntry: ppg ? rial(ppg / MESGHAL) : null,
|
||
receivedAt: received,
|
||
// The raw Jalali text is kept in the note when it did not parse.
|
||
note: [text(c.title), text(c.note), !received && text(c.recive_date) ? `تاریخ دریافت: ${text(c.recive_date)}` : null]
|
||
.filter(Boolean).join(' — ') || null,
|
||
createdAt: tehran(c.created_at) ?? undefined,
|
||
});
|
||
ctx.report.count(S, `document ${sourceType}${confirmed ? '' : ' (unconfirmed — not in the balance)'}`);
|
||
}
|
||
|
||
await balances(ctx);
|
||
}
|
||
|
||
/**
|
||
* Per migrated customer: compare what the import posted with the balance legacy
|
||
* stored, carry any real difference as one ADJUSTMENT entry, then set the
|
||
* cached balance to the sum of ALL confirmed entries (the rule every other
|
||
* ledger path keeps: confirmed DEBIT adds, confirmed CREDIT subtracts).
|
||
*/
|
||
async function balances(ctx: Ctx) {
|
||
const S = 'balances';
|
||
const legacy = await ctx.q<Row>('SELECT id, remain, rial_remain FROM customers WHERE deleted_at IS NULL');
|
||
const stored = new Map<string, { gold: number; rial: number }>();
|
||
for (const r of legacy) {
|
||
const target = ctx.map.get('customer', r.id);
|
||
if (!target) continue;
|
||
const cur = stored.get(target) ?? { gold: 0, rial: 0 };
|
||
cur.gold += toNumber(r.remain);
|
||
cur.rial += toNumber(r.rial_remain);
|
||
stored.set(target, cur);
|
||
}
|
||
|
||
const importedIds = new Set<string>();
|
||
for (const row of await ctx.db.$queryRawUnsafe<{ target_id: string }[]>(
|
||
`SELECT target_id FROM legacy_import.id_map WHERE entity = 'ledger' AND legacy_id NOT LIKE 'adjust:%'`,
|
||
)) importedIds.add(row.target_id);
|
||
|
||
// Up to this a difference is the double → Decimal(14,4) rounding of §4, not a
|
||
// real gap — inclusive: the first run posted two −0.0001 "adjustments" that
|
||
// were nothing but rounding, and would have shown on those statements.
|
||
const TOLERANCE = 0.0001;
|
||
|
||
for (const [customerId, want] of stored) {
|
||
const entries = await ctx.db.ledgerEntry.findMany({ where: { customerId, confirmed: true } });
|
||
const sum = (currency: LedgerCurrency, onlyImported: boolean) =>
|
||
entries
|
||
.filter((e) => e.currency === currency && (!onlyImported || importedIds.has(e.id)))
|
||
.reduce((acc, e) => (e.direction === LedgerDirection.DEBIT ? acc.add(e.amount) : acc.sub(e.amount)), new D(0));
|
||
|
||
for (const [currency, wanted, label] of [
|
||
[LedgerCurrency.GOLD_GRAM, want.gold, 'gold'],
|
||
[LedgerCurrency.RIAL, want.rial, 'rial'],
|
||
] as const) {
|
||
const key = `adjust:${customerId}:${label}`;
|
||
const diff = new D(wanted.toFixed(4)).sub(sum(currency, true));
|
||
const existing = ctx.map.get('ledger', key);
|
||
if (diff.abs().lte(TOLERANCE)) {
|
||
if (!diff.isZero()) ctx.report.count(S, `rounding-only difference (${label}, ≤ ${TOLERANCE}) — no adjustment`);
|
||
if (existing) {
|
||
await ctx.db.ledgerEntry.deleteMany({ where: { id: existing } });
|
||
await ctx.map.drop('ledger', key);
|
||
}
|
||
continue;
|
||
}
|
||
const data: Prisma.LedgerEntryUncheckedCreateInput = {
|
||
customerId,
|
||
currency,
|
||
direction: diff.gt(0) ? LedgerDirection.DEBIT : LedgerDirection.CREDIT,
|
||
amount: currency === LedgerCurrency.RIAL ? new D(diff.abs().toFixed(2)) : new D(diff.abs().toFixed(4)),
|
||
sourceType: LedgerSourceType.ADJUSTMENT,
|
||
confirmed: true,
|
||
confirmedAt: new Date(),
|
||
note: 'تراز انتقالی از سیستم قدیم — ماندهی ذخیرهشده در سیستم قدیم با اسناد قابل بازسازی نبود',
|
||
};
|
||
if (existing) await ctx.db.ledgerEntry.update({ where: { id: existing }, data });
|
||
else {
|
||
const created = await ctx.db.ledgerEntry.create({ data });
|
||
await ctx.map.set('ledger', key, created.id, 'created');
|
||
}
|
||
ctx.report.count(S, `opening-balance adjustment (${label})`);
|
||
ctx.report.note(S, `customer ${customerId}: ${label} adjustment ${diff.toString()} so the balance equals legacy's stored ${wanted.toFixed(4)}`);
|
||
}
|
||
|
||
// The cached balance = Σ every confirmed entry, the target's own and the imported ones.
|
||
const fresh = await ctx.db.ledgerEntry.findMany({ where: { customerId, confirmed: true } });
|
||
const total = (currency: LedgerCurrency) =>
|
||
fresh
|
||
.filter((e) => e.currency === currency)
|
||
.reduce((acc, e) => (e.direction === LedgerDirection.DEBIT ? acc.add(e.amount) : acc.sub(e.amount)), new D(0));
|
||
const before = await ctx.db.customer.findUniqueOrThrow({ where: { id: customerId }, select: { goldDebtGrams: true, fiatDebtRial: true } });
|
||
const gold = total(LedgerCurrency.GOLD_GRAM);
|
||
const rialSum = total(LedgerCurrency.RIAL);
|
||
if (!before.goldDebtGrams.eq(gold) || !before.fiatDebtRial.eq(rialSum)) {
|
||
await ctx.db.customer.update({ where: { id: customerId }, data: { goldDebtGrams: gold, fiatDebtRial: rialSum } });
|
||
ctx.report.count(S, 'cached balance set from the ledger');
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ───────────── favourites ───────────── */
|
||
|
||
/**
|
||
* The wholesale app's favourites are `major_favorites` (ApiMobileController
|
||
* :73-465, model Major\Favorite) — 3,305 rows. `fav` (251) belongs to the
|
||
* retail storefront (Customer\FavController, guard api-singleCustomer); its
|
||
* customer_id points at retail logins, so it is deliberately NOT read here.
|
||
*/
|
||
export async function favorites(ctx: Ctx) {
|
||
const S = 'favorites';
|
||
const rows = await ctx.q<Row>(
|
||
'SELECT customer_id, product_id, MIN(created_at) AS created_at FROM major_favorites WHERE deleted_at IS NULL GROUP BY customer_id, product_id',
|
||
);
|
||
const data: Prisma.CustomerFavoriteCreateManyInput[] = [];
|
||
const seen = new Set<string>();
|
||
for (const r of rows) {
|
||
const customerId = ctx.map.get('customer', r.customer_id);
|
||
const productId = ctx.map.get('product', r.product_id);
|
||
if (!customerId) { ctx.report.count(S, 'skipped: customer not migrated'); continue; }
|
||
if (!productId) { ctx.report.count(S, 'skipped: product deleted in legacy'); continue; }
|
||
const k = `${customerId}|${productId}`;
|
||
if (seen.has(k)) { ctx.report.count(S, 'duplicate collapsed (merged customers)'); continue; }
|
||
seen.add(k);
|
||
data.push({ customerId, productId, createdAt: tehran(r.created_at) ?? undefined });
|
||
}
|
||
const res = await ctx.db.customerFavorite.createMany({ data, skipDuplicates: true });
|
||
ctx.report.count(S, 'created', res.count);
|
||
ctx.report.count(S, 'already present', data.length - res.count);
|
||
}
|
||
|
||
/* ───────────── custom orders ───────────── */
|
||
|
||
/** CustomOrder::$STS_EN (CustomOrder.php:41-47) maps one-to-one. */
|
||
const PREORDER_STATUS: Record<number, PreOrderStatus> = {
|
||
1: PreOrderStatus.PLACED,
|
||
2: PreOrderStatus.ADMIN_APPROVED,
|
||
3: PreOrderStatus.WORKSHOP_APPROVED,
|
||
4: PreOrderStatus.READY_TO_SHIP,
|
||
5: PreOrderStatus.DELIVERED,
|
||
6: PreOrderStatus.CANCELLED,
|
||
};
|
||
|
||
export async function preOrders(ctx: Ctx) {
|
||
const S = 'custom orders';
|
||
const rows = await ctx.q<Row>('SELECT * FROM custom_orders WHERE deleted_at IS NULL');
|
||
for (const r of rows) {
|
||
const customerId = ctx.map.get('customer', r.customer_id);
|
||
if (!customerId) { ctx.report.count(S, 'skipped: customer not migrated'); continue; }
|
||
const sumText = text(r.sum);
|
||
const data = {
|
||
customerId,
|
||
cityId: ctx.map.get('city', r.city_id) ?? null,
|
||
workshopId: ctx.map.get('workshop', r.workshop_id) ?? null,
|
||
status: PREORDER_STATUS[int(r.sts) ?? 1] ?? PreOrderStatus.PLACED,
|
||
designImageUrl: await ctx.imageUrl(r.file_id, S),
|
||
colorName: text(r.color),
|
||
sizeSpec: text(r.size),
|
||
stoneColor: text(r.color_stone),
|
||
minaColor: text(r.color_mina),
|
||
estimatedWeightGrams: r.weight === null ? null : grams(r.weight),
|
||
estimatedPriceRial: sumText && /^\d+(\.\d+)?$/.test(sumText) ? rial(sumText) : null,
|
||
description: text(r.description),
|
||
};
|
||
const res = await upsert(ctx, 'preorder', r.id, {
|
||
exists: async (id) => !!(await ctx.db.preOrder.findUnique({ where: { id } })),
|
||
find: async () => null,
|
||
create: () => ctx.db.preOrder.create({ data: { ...data, createdAt: tehran(r.created_at) ?? undefined } }),
|
||
update: async (id) => {
|
||
await ctx.db.preOrder.update({ where: { id }, data });
|
||
},
|
||
});
|
||
ctx.report.count(S, res.isNew ? 'created' : 'updated');
|
||
}
|
||
}
|