Initial commit: Hedena Gold monorepo.
First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user