build: rebuild release bundles for orders pagination and dashboard hero
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+156
-59
@@ -80418,16 +80418,13 @@ var require_accounting_sync_service = __commonJS({
|
||||
select: { accountingGoldBalance: true, accountingRialBalance: true, accountingBalanceAt: true }
|
||||
});
|
||||
const stale = !c?.accountingBalanceAt || Date.now() - c.accountingBalanceAt.getTime() > BALANCE_FRESH_MS;
|
||||
if (stale) {
|
||||
if (c?.accountingBalanceAt && stale) {
|
||||
void this.refreshStoredBalance(a, customerId, link.externalCode).catch((err) => {
|
||||
this.logger.warn(`Background balance refresh for ${customerId} failed: ${err.message}`);
|
||||
});
|
||||
} else if (stale) {
|
||||
try {
|
||||
const [b] = await a.provider.getBalances({ ...a.settings, timeoutMs: Math.min(a.settings.timeoutMs, BALANCE_ON_DEMAND_TIMEOUT_MS) }, [link.externalCode]);
|
||||
if (b) {
|
||||
c = await this.prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: { accountingGoldBalance: b.goldGrams, accountingRialBalance: b.rial, accountingBalanceAt: /* @__PURE__ */ new Date() },
|
||||
select: { accountingGoldBalance: true, accountingRialBalance: true, accountingBalanceAt: true }
|
||||
});
|
||||
}
|
||||
c = await this.refreshStoredBalance(a, customerId, link.externalCode) ?? c;
|
||||
} catch (err) {
|
||||
this.logger.warn(`On-demand balance for ${customerId} failed, serving the stored one: ${err.message}`);
|
||||
}
|
||||
@@ -80441,6 +80438,25 @@ var require_accounting_sync_service = __commonJS({
|
||||
provider: a.provider.label
|
||||
};
|
||||
}
|
||||
/** Pull one linked customer's balance from the provider and persist it. */
|
||||
async refreshStoredBalance(a, customerId, externalCode) {
|
||||
const [b] = await a.provider.getBalances({ ...a.settings, timeoutMs: Math.min(a.settings.timeoutMs, BALANCE_ON_DEMAND_TIMEOUT_MS) }, [externalCode]);
|
||||
if (!b)
|
||||
return null;
|
||||
return this.prisma.customer.update({
|
||||
where: { id: customerId },
|
||||
data: {
|
||||
accountingGoldBalance: b.goldGrams,
|
||||
accountingRialBalance: b.rial,
|
||||
accountingBalanceAt: /* @__PURE__ */ new Date()
|
||||
},
|
||||
select: {
|
||||
accountingGoldBalance: true,
|
||||
accountingRialBalance: true,
|
||||
accountingBalanceAt: true
|
||||
}
|
||||
});
|
||||
}
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
// Real-time: change poll + live reads
|
||||
// ════════════════════════════════════════════════════════════════════
|
||||
@@ -81778,8 +81794,13 @@ var require_catalog_service = __commonJS({
|
||||
}
|
||||
if (query.categoryId)
|
||||
where.categoryId = query.categoryId;
|
||||
if (query.brandId)
|
||||
where.brandId = query.brandId;
|
||||
if (query.brandId) {
|
||||
const brandIds = query.brandId.split(",").map((s) => s.trim()).filter(Boolean);
|
||||
if (brandIds.length === 1)
|
||||
where.brandId = brandIds[0];
|
||||
else if (brandIds.length > 1)
|
||||
where.brandId = { in: brandIds };
|
||||
}
|
||||
if (query.collectionId)
|
||||
where.collections = { some: { collectionId: query.collectionId } };
|
||||
if (query.minGram !== void 0 || query.maxGram !== void 0) {
|
||||
@@ -83215,11 +83236,16 @@ var require_customers_service = __commonJS({
|
||||
var __metadata3 = exports2 && exports2.__metadata || function(k, v) {
|
||||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
||||
};
|
||||
var CustomersService_1;
|
||||
Object.defineProperty(exports2, "__esModule", { value: true });
|
||||
exports2.CustomersService = void 0;
|
||||
var common_12 = require_common();
|
||||
var client_1 = require("@prisma/client");
|
||||
var prisma_service_1 = require_prisma_service();
|
||||
var accounting_sync_service_1 = require_accounting_sync_service();
|
||||
var ORDERS_PAGE_LIMIT = 20;
|
||||
var ORDERS_PAGE_LIMIT_MAX = 50;
|
||||
var ORDER_STATUSES = new Set(Object.values(client_1.OrderStatus));
|
||||
var PUBLIC_CUSTOMER_SELECT = {
|
||||
id: true,
|
||||
fullName: true,
|
||||
@@ -83239,9 +83265,10 @@ var require_customers_service = __commonJS({
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
};
|
||||
var CustomersService = class CustomersService {
|
||||
var CustomersService = CustomersService_1 = class CustomersService {
|
||||
prisma;
|
||||
accounting;
|
||||
logger = new common_12.Logger(CustomersService_1.name);
|
||||
constructor(prisma, accounting) {
|
||||
this.prisma = prisma;
|
||||
this.accounting = accounting;
|
||||
@@ -83253,51 +83280,91 @@ var require_customers_service = __commonJS({
|
||||
});
|
||||
if (!customer)
|
||||
throw new common_12.NotFoundException(`Customer ${customerId} not found`);
|
||||
const accounting = await this.accounting.customerBalance(customerId);
|
||||
if (accounting) {
|
||||
return {
|
||||
...customer,
|
||||
goldDebtGrams: accounting.gold,
|
||||
fiatDebtRial: accounting.rial,
|
||||
balanceSource: "accounting",
|
||||
balanceAt: accounting.at
|
||||
};
|
||||
try {
|
||||
const accounting = await this.accounting.customerBalance(customerId);
|
||||
if (accounting) {
|
||||
return {
|
||||
...customer,
|
||||
goldDebtGrams: accounting.gold,
|
||||
fiatDebtRial: accounting.rial,
|
||||
balanceSource: "accounting",
|
||||
balanceAt: accounting.at
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`Balance overlay for ${customerId} failed; serving app ledger: ${err.message}`);
|
||||
}
|
||||
return { ...customer, balanceSource: "app", balanceAt: null };
|
||||
}
|
||||
getOrders(customerId) {
|
||||
return this.prisma.order.findMany({
|
||||
where: { customerId },
|
||||
include: {
|
||||
items: {
|
||||
// Each item carried only `productVariantId`, so the apps looked the
|
||||
// title up by loading the ENTIRE catalogue and joining in the browser
|
||||
// (composables/useOrderItemProduct.ts). With ~16k products that is
|
||||
// the wrong trade for a few row labels, and it also failed for an
|
||||
// order whose product has since left the storefront — the catalogue
|
||||
// hides it, so the row rendered blank.
|
||||
//
|
||||
// A nested read here is one join and always answers, including for
|
||||
// products that are no longer on sale.
|
||||
include: {
|
||||
productVariant: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
images: { orderBy: { sortOrder: "asc" }, take: 1, select: { url: true } }
|
||||
}
|
||||
/**
|
||||
* Paginated order history for the signed-in customer.
|
||||
*
|
||||
* Shape matches the report endpoints (`result` / `count` / `page` / `limit`)
|
||||
* so the apps can infinite-scroll the same way. Status and search are applied
|
||||
* server-side — client-side filtering after a page slice would miss matches
|
||||
* that sit on later pages.
|
||||
*/
|
||||
async getOrders(customerId, query = {}) {
|
||||
const page = Math.max(1, Math.floor(query.page ?? 1) || 1);
|
||||
const limit = Math.min(ORDERS_PAGE_LIMIT_MAX, Math.max(1, Math.floor(query.limit ?? ORDERS_PAGE_LIMIT) || ORDERS_PAGE_LIMIT));
|
||||
const where = { customerId };
|
||||
if (query.status && query.status !== "ALL" && ORDER_STATUSES.has(query.status)) {
|
||||
where.status = query.status;
|
||||
}
|
||||
const q = query.q?.trim();
|
||||
if (q) {
|
||||
where.orderNumber = { contains: q, mode: "insensitive" };
|
||||
}
|
||||
const include = {
|
||||
items: {
|
||||
// Each item carried only `productVariantId`, so the apps looked the
|
||||
// title up by loading the ENTIRE catalogue and joining in the browser
|
||||
// (composables/useOrderItemProduct.ts). With ~16k products that is
|
||||
// the wrong trade for a few row labels, and it also failed for an
|
||||
// order whose product has since left the storefront — the catalogue
|
||||
// hides it, so the row rendered blank.
|
||||
//
|
||||
// A nested read here is one join and always answers, including for
|
||||
// products that are no longer on sale.
|
||||
include: {
|
||||
productVariant: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
product: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
images: { orderBy: { sortOrder: "asc" }, take: 1, select: { url: true } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: { createdAt: "desc" }
|
||||
});
|
||||
// Chronological status changes for the order-detail timeline in the app.
|
||||
statusEvents: {
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
fromStatus: true,
|
||||
toStatus: true,
|
||||
note: true,
|
||||
createdAt: true
|
||||
}
|
||||
}
|
||||
};
|
||||
const [result, count] = await Promise.all([
|
||||
this.prisma.order.findMany({
|
||||
where,
|
||||
include,
|
||||
orderBy: { createdAt: "desc" },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
}),
|
||||
this.prisma.order.count({ where })
|
||||
]);
|
||||
return { result, count, page, limit };
|
||||
}
|
||||
getLedgerEntries(customerId) {
|
||||
return this.prisma.ledgerEntry.findMany({
|
||||
@@ -83307,7 +83374,7 @@ var require_customers_service = __commonJS({
|
||||
}
|
||||
};
|
||||
exports2.CustomersService = CustomersService;
|
||||
exports2.CustomersService = CustomersService = __decorate4([
|
||||
exports2.CustomersService = CustomersService = CustomersService_1 = __decorate4([
|
||||
(0, common_12.Injectable)(),
|
||||
__metadata3("design:paramtypes", [
|
||||
prisma_service_1.PrismaService,
|
||||
@@ -88612,7 +88679,7 @@ var require_brand = __commonJS({
|
||||
exports2.brandSiteUrl = brandSiteUrl;
|
||||
var env_12 = require_env();
|
||||
function brandName() {
|
||||
return (0, env_12.envOr)("BRAND_NAME", "\u0632\u0631\u062A\u0627\u062C");
|
||||
return (0, env_12.envOr)("BRAND_NAME", "\u062A\u0647\u0631\u0627\u0646 \u062A\u0627\u062C");
|
||||
}
|
||||
function brandSiteUrl() {
|
||||
return (0, env_12.envOr)("BRAND_SITE_URL", "https://bnk.montzar.ir");
|
||||
@@ -89617,8 +89684,13 @@ var require_customer_auth_controller = __commonJS({
|
||||
me(me) {
|
||||
return this.customers.getOrThrow(me.id);
|
||||
}
|
||||
myOrders(me) {
|
||||
return this.customers.getOrders(me.id);
|
||||
myOrders(me, page, limit, status, q) {
|
||||
return this.customers.getOrders(me.id, {
|
||||
page: Number(page) || 1,
|
||||
limit: limit !== void 0 && limit !== "" ? Number(limit) : void 0,
|
||||
status,
|
||||
q
|
||||
});
|
||||
}
|
||||
myLedgerEntries(me) {
|
||||
return this.customers.getLedgerEntries(me.id);
|
||||
@@ -89705,8 +89777,12 @@ var require_customer_auth_controller = __commonJS({
|
||||
(0, customer_guards_1.CustomerAuth)(),
|
||||
(0, common_12.Get)("me/orders"),
|
||||
__param3(0, (0, customer_guards_1.CurrentCustomer)()),
|
||||
__param3(1, (0, common_12.Query)("page")),
|
||||
__param3(2, (0, common_12.Query)("limit")),
|
||||
__param3(3, (0, common_12.Query)("status")),
|
||||
__param3(4, (0, common_12.Query)("q")),
|
||||
__metadata3("design:type", Function),
|
||||
__metadata3("design:paramtypes", [Object]),
|
||||
__metadata3("design:paramtypes", [Object, String, String, String, String]),
|
||||
__metadata3("design:returntype", void 0)
|
||||
], CustomerAuthController.prototype, "myOrders", null);
|
||||
__decorate4([
|
||||
@@ -92010,13 +92086,17 @@ var require_orders_service = __commonJS({
|
||||
const flatRialCharge = computedItems.reduce((sum, item) => sum.add(item.flatRialCharge.mul(item.quantity)), new client_1.Prisma.Decimal(0));
|
||||
const isSuspendedOverCreditLimit = totalGoldWeightGrams.add(customer.goldDebtGrams).gt(customer.goldCreditLimitGrams);
|
||||
const initialStatus = isSuspendedOverCreditLimit ? client_1.OrderStatus.SUSPENDED : client_1.OrderStatus.PLACED;
|
||||
const channel = opts.placedBy === "customer" ? client_1.OrderChannel.WHOLESALE : dto.channel ?? client_1.OrderChannel.RETAIL;
|
||||
if (opts.placedBy === "customer" && dto.channel && dto.channel !== client_1.OrderChannel.WHOLESALE) {
|
||||
this.logger.warn(`Order body carried channel=${dto.channel} from the app; forcing WHOLESALE.`);
|
||||
}
|
||||
const placed = await this.prisma.$transaction(async (tx) => {
|
||||
const order = await tx.order.create({
|
||||
data: {
|
||||
orderNumber: this.generateOrderNumber(),
|
||||
orderNumber: await this.nextOrderNumber(tx),
|
||||
customerId,
|
||||
branchId: dto.branchId,
|
||||
channel: dto.channel ?? client_1.OrderChannel.RETAIL,
|
||||
channel,
|
||||
status: initialStatus,
|
||||
totalGoldWeightGrams,
|
||||
wageWeightGrams,
|
||||
@@ -92181,9 +92261,22 @@ var require_orders_service = __commonJS({
|
||||
throw new common_12.NotFoundException("\u0633\u0641\u0627\u0631\u0634 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
|
||||
return order;
|
||||
}
|
||||
generateOrderNumber() {
|
||||
const random = Math.random().toString(36).slice(2, 8).toUpperCase();
|
||||
return `ORD-${Date.now()}-${random}`;
|
||||
/**
|
||||
* Short global sequential number for the customer, SMS and search
|
||||
* (`100`, `101`, …). Allocated from Postgres `order_number_seq` inside the
|
||||
* placement transaction so concurrent creates cannot collide. Legacy used
|
||||
* the auto-increment order id the same way; the old `ORD-{ts}-{rand}` form
|
||||
* was too long for SMS and the list UI.
|
||||
*/
|
||||
async nextOrderNumber(tx) {
|
||||
const rows = await tx.$queryRaw`
|
||||
SELECT nextval('order_number_seq') AS n
|
||||
`;
|
||||
const n = rows[0]?.n;
|
||||
if (n === void 0 || n === null) {
|
||||
throw new Error("order_number_seq returned no value");
|
||||
}
|
||||
return String(n);
|
||||
}
|
||||
};
|
||||
exports2.OrdersService = OrdersService;
|
||||
@@ -92269,7 +92362,11 @@ var require_create_order_dto = __commonJS({
|
||||
/**
|
||||
* Which sales channel placed the order. Legacy had no such field because the
|
||||
* two channels lived in separate tables (orders vs orders_orders); the one
|
||||
* Order model carries the distinction here. Defaults to RETAIL.
|
||||
* Order model carries the distinction here.
|
||||
*
|
||||
* On the customer (app) path the server forces WHOLESALE and ignores this
|
||||
* field. Operators may set it; admin MakeOrder always sends WHOLESALE.
|
||||
* Default when an operator omits it remains RETAIL (retail list builder).
|
||||
*/
|
||||
channel;
|
||||
items;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
-- App (B2B) orders were stored as RETAIL because createOrder defaulted
|
||||
-- channel to RETAIL and the apps never sent one. Legacy's mobile setOrder
|
||||
-- wrote to the wholesale `orders` table.
|
||||
--
|
||||
-- Flip mis-filed rows: any RETAIL order whose customer is NOT a تک
|
||||
-- (CustomerKind.RETAIL) belongs on the omde list. Genuine retail-list
|
||||
-- orders for CustomerKind.RETAIL stay RETAIL.
|
||||
|
||||
UPDATE "Order" AS o
|
||||
SET channel = 'WHOLESALE'
|
||||
FROM "Customer" AS c
|
||||
WHERE o."customerId" = c.id
|
||||
AND o.channel = 'RETAIL'
|
||||
AND c.kind <> 'RETAIL';
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Short global customer-facing order numbers (SMS / app), replacing
|
||||
-- ORD-{timestamp}-{random}. Pure digits, starting at 100.
|
||||
-- Existing ORD-* / MOCK-ORD-* rows stay as-is; the sequence only advances
|
||||
-- past any already-numeric orderNumber so new allocations never collide.
|
||||
|
||||
DROP SEQUENCE IF EXISTS order_number_seq;
|
||||
|
||||
CREATE SEQUENCE order_number_seq
|
||||
AS BIGINT
|
||||
INCREMENT BY 1
|
||||
MINVALUE 1
|
||||
START WITH 100
|
||||
NO CYCLE;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
max_numeric bigint;
|
||||
BEGIN
|
||||
SELECT MAX("orderNumber"::bigint)
|
||||
INTO max_numeric
|
||||
FROM "Order"
|
||||
WHERE "orderNumber" ~ '^[0-9]+$';
|
||||
|
||||
IF max_numeric IS NULL THEN
|
||||
-- next nextval() returns 100
|
||||
PERFORM setval('order_number_seq', 100, false);
|
||||
ELSE
|
||||
-- next nextval() returns max_numeric + 1
|
||||
PERFORM setval('order_number_seq', max_numeric, true);
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -2086,8 +2086,8 @@ async function main() {
|
||||
await prisma.brand.updateMany({ where: { id: "brand-henza" }, data: { id: "brand-montzar" } });
|
||||
const brandMain = await prisma.brand.upsert({
|
||||
where: { id: "brand-montzar" },
|
||||
update: { title: "\u0632\u0631\u062A\u0627\u062C \u0637\u0644\u0627" },
|
||||
create: { id: "brand-montzar", title: "\u0632\u0631\u062A\u0627\u062C \u0637\u0644\u0627" }
|
||||
update: { title: "\u062A\u0647\u0631\u0627\u0646 \u062A\u0627\u062C \u0637\u0644\u0627" },
|
||||
create: { id: "brand-montzar", title: "\u062A\u0647\u0631\u0627\u0646 \u062A\u0627\u062C \u0637\u0644\u0627" }
|
||||
});
|
||||
const brandArtisan = await prisma.brand.upsert({
|
||||
where: { id: "brand-artisan" },
|
||||
|
||||
Reference in New Issue
Block a user