@infuro/cms-core 1.0.42 → 1.0.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -452,6 +452,41 @@ function applyApprovalStatusSideEffects(updatePayload, opts) {
452
452
  }
453
453
  }
454
454
  __name(applyApprovalStatusSideEffects, "applyApprovalStatusSideEffects");
455
+ async function syncProductVariantsStatusWithProduct(dataSource, entityMap, productId, productStatus) {
456
+ if (!entityMap.product_variants || !Number.isFinite(productId) || productId < 1) return;
457
+ const status = String(productStatus || "draft");
458
+ const repo = dataSource.getRepository(entityMap.product_variants);
459
+ if (status === "available") {
460
+ await repo.createQueryBuilder().update().set({
461
+ status: "available"
462
+ }).where('"productId" = :productId', {
463
+ productId
464
+ }).andWhere("status = :from", {
465
+ from: "draft"
466
+ }).execute();
467
+ return;
468
+ }
469
+ if (status === "draft") {
470
+ await repo.createQueryBuilder().update().set({
471
+ status: "draft"
472
+ }).where('"productId" = :productId', {
473
+ productId
474
+ }).andWhere("status = :from", {
475
+ from: "available"
476
+ }).execute();
477
+ }
478
+ }
479
+ __name(syncProductVariantsStatusWithProduct, "syncProductVariantsStatusWithProduct");
480
+ function coerceVariantStatusForProduct(variantStatus, productStatus, opts) {
481
+ const next = String(variantStatus ?? "draft").trim() || "draft";
482
+ const productLive = String(productStatus ?? "draft") === "available";
483
+ const approvalOk = !opts?.requireApproval || String(opts?.approvalStatus ?? "") === "approved";
484
+ if (!productLive || !approvalOk) {
485
+ return "draft";
486
+ }
487
+ return next;
488
+ }
489
+ __name(coerceVariantStatusForProduct, "coerceVariantStatusForProduct");
455
490
 
456
491
  // src/lib/event-approval.ts
457
492
  var EVENT_APPROVAL_STATUSES = [
@@ -4594,6 +4629,26 @@ function createCrudHandler(dataSource, entityMap, options) {
4594
4629
  }
4595
4630
  }
4596
4631
  }
4632
+ if (resource === "product_variants" && entityMap.products) {
4633
+ const productIdRaw = persistBody.productId;
4634
+ const productId = typeof productIdRaw === "number" ? productIdRaw : typeof productIdRaw === "string" && /^\d+$/.test(productIdRaw) ? parseInt(productIdRaw, 10) : NaN;
4635
+ if (Number.isFinite(productId)) {
4636
+ const parent = await dataSource.getRepository(entityMap.products).findOne({
4637
+ where: {
4638
+ id: productId
4639
+ }
4640
+ });
4641
+ const parentStatus = parent?.status;
4642
+ const parentApproval = parent?.approvalStatus;
4643
+ const requireApproval = await getRequireProductApproval(dataSource);
4644
+ persistBody.status = coerceVariantStatusForProduct(persistBody.status, parentStatus, {
4645
+ approvalStatus: parentApproval,
4646
+ requireApproval
4647
+ });
4648
+ } else if (!("status" in persistBody)) {
4649
+ persistBody.status = "draft";
4650
+ }
4651
+ }
4597
4652
  if (resource === "products") {
4598
4653
  const scopeForProduct = await resolveScope();
4599
4654
  const productFlags = await getVendorCatalogCreateFlags(dataSource);
@@ -6233,6 +6288,34 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6233
6288
  delete u.parentId;
6234
6289
  }
6235
6290
  }
6291
+ if (resource === "product_variants" && entityMap.products) {
6292
+ const currentVariant = await repo.findOne({
6293
+ where: {
6294
+ id: numericId
6295
+ }
6296
+ });
6297
+ if (!currentVariant) return json({
6298
+ message: "Not found"
6299
+ }, {
6300
+ status: 404
6301
+ });
6302
+ const productId = "productId" in updatePayload && updatePayload.productId != null ? Number(updatePayload.productId) : Number(currentVariant.productId);
6303
+ if (Number.isFinite(productId)) {
6304
+ const parent = await dataSource.getRepository(entityMap.products).findOne({
6305
+ where: {
6306
+ id: productId
6307
+ }
6308
+ });
6309
+ const parentStatus = parent?.status;
6310
+ const parentApproval = parent?.approvalStatus;
6311
+ const requireApproval = await getRequireProductApproval(dataSource);
6312
+ const nextStatus = "status" in updatePayload ? updatePayload.status : currentVariant.status;
6313
+ updatePayload.status = coerceVariantStatusForProduct(nextStatus, parentStatus, {
6314
+ approvalStatus: parentApproval,
6315
+ requireApproval
6316
+ });
6317
+ }
6318
+ }
6236
6319
  if (resource === "products") {
6237
6320
  const currentRow = await repo.findOne({
6238
6321
  where: {
@@ -6588,6 +6671,10 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6588
6671
  if (reloaded) updated = reloaded;
6589
6672
  }
6590
6673
  updated = hydrateProductDisplayName(updated);
6674
+ const productStatus = String(updated.status ?? "draft");
6675
+ if (Number.isFinite(updatedId) && updatedId > 0) {
6676
+ await syncProductVariantsStatusWithProduct(dataSource, entityMap, updatedId, productStatus);
6677
+ }
6591
6678
  if (getCms) {
6592
6679
  const cms = await getCms();
6593
6680
  await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, updated);
@@ -8625,6 +8712,12 @@ function createUploadHandler(config) {
8625
8712
  parentId
8626
8713
  });
8627
8714
  } catch (err) {
8715
+ console.error("[upload] File upload failed", {
8716
+ error: err instanceof Error ? err.message : String(err),
8717
+ stack: err instanceof Error ? err.stack : void 0,
8718
+ name: err instanceof Error ? err.name : void 0,
8719
+ metadata: err && typeof err === "object" && "$metadata" in err ? err.$metadata : void 0
8720
+ });
8628
8721
  return json({
8629
8722
  error: "File upload failed"
8630
8723
  }, {
@@ -9330,41 +9423,68 @@ function createUsersApiHandlers(config) {
9330
9423
  const page = Math.max(1, parseInt(url.searchParams.get("page") || "1", 10));
9331
9424
  const limit = Math.min(100, parseInt(url.searchParams.get("limit") || "10", 10));
9332
9425
  const skip = (page - 1) * limit;
9333
- const sortField = url.searchParams.get("sortField") || "createdAt";
9426
+ const sortFieldRaw = url.searchParams.get("sortField") || "createdAt";
9427
+ const allowedSort = /* @__PURE__ */ new Set([
9428
+ "createdAt",
9429
+ "updatedAt",
9430
+ "name",
9431
+ "email",
9432
+ "id",
9433
+ "blocked"
9434
+ ]);
9435
+ const sortField = allowedSort.has(sortFieldRaw) ? sortFieldRaw : "createdAt";
9334
9436
  const sortOrder = url.searchParams.get("sortOrder") === "desc" ? "DESC" : "ASC";
9335
- const search = url.searchParams.get("search");
9336
- const where = search ? [
9337
- {
9338
- name: ILike(`%${search}%`),
9339
- deleted: false
9340
- },
9341
- {
9342
- email: ILike(`%${search}%`),
9343
- deleted: false
9344
- }
9345
- ] : {
9437
+ const search = url.searchParams.get("search")?.trim() || "";
9438
+ const groupName = url.searchParams.get("groupName")?.trim() || "";
9439
+ const qb = userRepo().createQueryBuilder("u").leftJoinAndSelect("u.group", "g").where("u.deleted = :deleted", {
9346
9440
  deleted: false
9347
- };
9348
- const [data, total] = await userRepo().findAndCount({
9349
- skip,
9350
- take: limit,
9351
- order: {
9352
- [sortField]: sortOrder
9353
- },
9354
- where,
9355
- relations: [
9356
- "group"
9357
- ],
9358
- select: [
9359
- "id",
9360
- "name",
9361
- "email",
9362
- "blocked",
9363
- "createdAt",
9364
- "updatedAt",
9365
- "groupId"
9366
- ]
9367
- });
9441
+ }).orderBy(`u.${sortField}`, sortOrder).skip(skip).take(limit).select([
9442
+ "u.id",
9443
+ "u.name",
9444
+ "u.email",
9445
+ "u.blocked",
9446
+ "u.createdAt",
9447
+ "u.updatedAt",
9448
+ "u.groupId",
9449
+ "g.id",
9450
+ "g.name"
9451
+ ]);
9452
+ if (search) {
9453
+ qb.andWhere("(u.name ILIKE :search OR u.email ILIKE :search)", {
9454
+ search: `%${search}%`
9455
+ });
9456
+ }
9457
+ if (groupName) {
9458
+ if (!entityMap.user_groups) {
9459
+ return json({
9460
+ total: 0,
9461
+ page,
9462
+ limit,
9463
+ totalPages: 0,
9464
+ data: []
9465
+ });
9466
+ }
9467
+ const groupRepo = dataSource.getRepository(entityMap.user_groups);
9468
+ const group = await groupRepo.findOne({
9469
+ where: {
9470
+ name: groupName,
9471
+ deleted: false
9472
+ }
9473
+ });
9474
+ if (!group) {
9475
+ return json({
9476
+ total: 0,
9477
+ page,
9478
+ limit,
9479
+ totalPages: 0,
9480
+ data: []
9481
+ });
9482
+ }
9483
+ qb.andWhere("u.groupId = :groupId", {
9484
+ groupId: Number(group.id)
9485
+ });
9486
+ }
9487
+ const [data, total] = await qb.getManyAndCount();
9368
9488
  return json({
9369
9489
  total,
9370
9490
  page,
@@ -29772,7 +29892,8 @@ function createStorefrontApiHandler(config) {
29772
29892
  const repo = dataSource.getRepository(entityMap.product_variants);
29773
29893
  const rows = await repo.find({
29774
29894
  where: {
29775
- productId
29895
+ productId,
29896
+ status: "available"
29776
29897
  },
29777
29898
  order: {
29778
29899
  id: "ASC"
@@ -29805,6 +29926,52 @@ function createStorefrontApiHandler(config) {
29805
29926
  return result;
29806
29927
  }
29807
29928
  __name(loadProductVariantsWithPricing, "loadProductVariantsWithPricing");
29929
+ async function loadApplicableVendorPolicies(productId, vendorId) {
29930
+ if (!entityMap.refund_policies) return [];
29931
+ const policyRepo = dataSource.getRepository(entityMap.refund_policies);
29932
+ const format = /* @__PURE__ */ __name((row) => ({
29933
+ id: Number(row.id),
29934
+ name: String(row.name ?? "").trim(),
29935
+ desc: row.desc != null ? String(row.desc) : null,
29936
+ refundWindowDays: Number(row.refundWindowDays) || 0,
29937
+ type: String(row.type ?? "percentage"),
29938
+ value: Number(row.value) || 0
29939
+ }), "format");
29940
+ const linkedIds = /* @__PURE__ */ new Set();
29941
+ if (entityMap.product_config) {
29942
+ const configs = await dataSource.getRepository(entityMap.product_config).find({
29943
+ where: {
29944
+ productId
29945
+ }
29946
+ });
29947
+ for (const c of configs) {
29948
+ const id = Number(c.refundPolicyId);
29949
+ if (Number.isFinite(id) && id > 0) linkedIds.add(id);
29950
+ }
29951
+ }
29952
+ if (linkedIds.size > 0) {
29953
+ const rows = await policyRepo.find({
29954
+ where: {
29955
+ id: In([
29956
+ ...linkedIds
29957
+ ])
29958
+ }
29959
+ });
29960
+ return rows.map((r) => format(r)).filter((p) => p.name).sort((a, b) => a.id - b.id);
29961
+ }
29962
+ if (vendorId == null || !Number.isFinite(Number(vendorId))) return [];
29963
+ const vendorRows = await policyRepo.find({
29964
+ where: {
29965
+ vendorId: Number(vendorId),
29966
+ status: "active"
29967
+ },
29968
+ order: {
29969
+ id: "ASC"
29970
+ }
29971
+ });
29972
+ return vendorRows.map((r) => format(r)).filter((p) => p.name);
29973
+ }
29974
+ __name(loadApplicableVendorPolicies, "loadApplicableVendorPolicies");
29808
29975
  return {
29809
29976
  async handle(method, path2, req) {
29810
29977
  try {
@@ -29872,13 +30039,11 @@ function createStorefrontApiHandler(config) {
29872
30039
  const url = new URL(req.url || "", "http://localhost");
29873
30040
  const collectionSlug = url.searchParams.get("collection")?.trim();
29874
30041
  const collectionId = url.searchParams.get("collectionId");
30042
+ const q = url.searchParams.get("q")?.trim() ?? "";
29875
30043
  const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20", 10)));
29876
30044
  const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
29877
- const where = {
29878
- status: "available",
29879
- deleted: false
29880
- };
29881
30045
  let collectionFilter = null;
30046
+ let collectionIdFilter = null;
29882
30047
  if (collectionSlug) {
29883
30048
  let col = null;
29884
30049
  if (/^\d+$/.test(collectionSlug)) {
@@ -29905,30 +30070,59 @@ function createStorefrontApiHandler(config) {
29905
30070
  collection: null
29906
30071
  });
29907
30072
  }
29908
- where.collectionId = col.id;
30073
+ collectionIdFilter = Number(col.id);
29909
30074
  collectionFilter = {
29910
30075
  name: col.name,
29911
30076
  slug: col.slug
29912
30077
  };
29913
30078
  } else if (collectionId) {
29914
30079
  const cid = parseInt(collectionId, 10);
29915
- if (Number.isFinite(cid)) where.collectionId = cid;
30080
+ if (Number.isFinite(cid)) collectionIdFilter = cid;
30081
+ }
30082
+ let items = [];
30083
+ let total = 0;
30084
+ if (q) {
30085
+ const like = `%${q.replace(/[%_]/g, "\\$&")}%`;
30086
+ const qb = productRepo().createQueryBuilder("p").where("p.status = :status", {
30087
+ status: "available"
30088
+ }).andWhere("p.deleted = :del", {
30089
+ del: false
30090
+ }).andWhere(`(p.name ILIKE :like OR p.slug ILIKE :like OR COALESCE(p.sku, '') ILIKE :like OR COALESCE(p.title, '') ILIKE :like OR COALESCE(p.metadata->>'description', '') ILIKE :like)`, {
30091
+ like
30092
+ }).orderBy("p.id", "ASC").take(limit).skip(offset);
30093
+ if (collectionIdFilter != null) {
30094
+ qb.andWhere("p.collectionId = :cid", {
30095
+ cid: collectionIdFilter
30096
+ });
30097
+ }
30098
+ [items, total] = await qb.getManyAndCount();
30099
+ } else {
30100
+ const where = {
30101
+ status: "available",
30102
+ deleted: false
30103
+ };
30104
+ if (collectionIdFilter != null) where.collectionId = collectionIdFilter;
30105
+ const result = await productRepo().findAndCount({
30106
+ where,
30107
+ order: {
30108
+ id: "ASC"
30109
+ },
30110
+ take: limit,
30111
+ skip: offset
30112
+ });
30113
+ items = result[0];
30114
+ total = result[1];
29916
30115
  }
29917
- const [items, total] = await productRepo().findAndCount({
29918
- where,
29919
- order: {
29920
- id: "ASC"
29921
- },
29922
- take: limit,
29923
- skip: offset
29924
- });
29925
30116
  const products = await Promise.all(items.map((item) => enrichProductPricing(item)));
29926
30117
  return json({
29927
30118
  products,
29928
30119
  total,
29929
30120
  ...collectionFilter && {
29930
30121
  collection: collectionFilter
29931
- }
30122
+ },
30123
+ ...q ? {
30124
+ q
30125
+ } : {}
29932
30126
  });
29933
30127
  }
29934
30128
  if (path2[0] === "products" && path2.length === 2 && method === "GET") {
@@ -29964,12 +30158,14 @@ function createStorefrontApiHandler(config) {
29964
30158
  const pricing = await resolveProductEventPricing(Number(p.id));
29965
30159
  const enriched = await enrichProductPricing(p, pricing);
29966
30160
  const variants = await loadProductVariantsWithPricing(Number(p.id), pricing);
30161
+ const policies = await loadApplicableVendorPolicies(Number(p.id), p.vendorId != null ? Number(p.vendorId) : null);
29967
30162
  return json({
29968
30163
  ...enriched,
29969
30164
  attributes: attributeTags,
29970
30165
  ...variants.length ? {
29971
30166
  variants
29972
- } : {}
30167
+ } : {},
30168
+ policies
29973
30169
  });
29974
30170
  }
29975
30171
  if (path2[0] === "collections" && path2.length === 1 && method === "GET") {
@@ -30781,24 +30977,61 @@ function createStorefrontApiHandler(config) {
30781
30977
  }, {
30782
30978
  status: 404
30783
30979
  });
30980
+ const rawVariantId = body.variantId ?? body.variant_id;
30981
+ const variantIdNum = rawVariantId != null && String(rawVariantId).trim() !== "" ? Number(rawVariantId) : NaN;
30982
+ const hasVariantId = Number.isFinite(variantIdNum) && variantIdNum > 0;
30983
+ const bodyMeta = body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? body.metadata : {};
30984
+ const optionsRaw = body.options && typeof body.options === "object" && !Array.isArray(body.options) ? body.options : bodyMeta.options && typeof bodyMeta.options === "object" && !Array.isArray(bodyMeta.options) ? bodyMeta.options : null;
30985
+ const options = optionsRaw && Object.fromEntries(Object.entries(optionsRaw).map(([k, v]) => [
30986
+ String(k),
30987
+ String(v ?? "").trim()
30988
+ ]).filter(([, v]) => v.length > 0));
30989
+ const lineMetadata = {
30990
+ ...bodyMeta,
30991
+ ...hasVariantId ? {
30992
+ variantId: variantIdNum
30993
+ } : {},
30994
+ ...options && Object.keys(options).length ? {
30995
+ options
30996
+ } : {}
30997
+ };
30998
+ if (typeof bodyMeta.imageUrl === "string" && bodyMeta.imageUrl.trim()) {
30999
+ lineMetadata.imageUrl = bodyMeta.imageUrl.trim();
31000
+ }
31001
+ if (typeof bodyMeta.title === "string" && bodyMeta.title.trim()) {
31002
+ lineMetadata.title = bodyMeta.title.trim();
31003
+ }
31004
+ const metadataPayload = Object.keys(lineMetadata).length ? lineMetadata : null;
30784
31005
  const { cart, setCookie, err } = await getOrCreateCart(req);
30785
31006
  if (err) return err;
30786
31007
  const cartId = cart.id;
30787
- const existing = await cartItemRepo().findOne({
31008
+ const sameProductLines = await cartItemRepo().find({
30788
31009
  where: {
30789
31010
  cartId,
30790
31011
  productId
30791
31012
  }
30792
31013
  });
31014
+ const existing = sameProductLines.find((row) => {
31015
+ const m = row.metadata;
31016
+ const existingVid = m?.variantId ?? m?.variant_id;
31017
+ if (hasVariantId) {
31018
+ return Number(existingVid) === variantIdNum;
31019
+ }
31020
+ return existingVid == null || existingVid === "";
31021
+ });
30793
31022
  if (existing) {
30794
31023
  await cartItemRepo().update(existing.id, {
30795
- quantity: existing.quantity + quantity
31024
+ quantity: existing.quantity + quantity,
31025
+ ...metadataPayload ? {
31026
+ metadata: metadataPayload
31027
+ } : {}
30796
31028
  });
30797
31029
  } else {
30798
31030
  await cartItemRepo().save(cartItemRepo().create({
30799
31031
  cartId,
30800
31032
  productId,
30801
- quantity
31033
+ quantity,
31034
+ metadata: metadataPayload
30802
31035
  }));
30803
31036
  }
30804
31037
  await cartRepo().update(cartId, {