@infuro/cms-core 1.0.43 → 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.
@@ -462,6 +462,41 @@ function applyApprovalStatusSideEffects(updatePayload, opts) {
462
462
  }
463
463
  }
464
464
  chunkUSNT2KNT_cjs.__name(applyApprovalStatusSideEffects, "applyApprovalStatusSideEffects");
465
+ async function syncProductVariantsStatusWithProduct(dataSource, entityMap, productId, productStatus) {
466
+ if (!entityMap.product_variants || !Number.isFinite(productId) || productId < 1) return;
467
+ const status = String(productStatus || "draft");
468
+ const repo = dataSource.getRepository(entityMap.product_variants);
469
+ if (status === "available") {
470
+ await repo.createQueryBuilder().update().set({
471
+ status: "available"
472
+ }).where('"productId" = :productId', {
473
+ productId
474
+ }).andWhere("status = :from", {
475
+ from: "draft"
476
+ }).execute();
477
+ return;
478
+ }
479
+ if (status === "draft") {
480
+ await repo.createQueryBuilder().update().set({
481
+ status: "draft"
482
+ }).where('"productId" = :productId', {
483
+ productId
484
+ }).andWhere("status = :from", {
485
+ from: "available"
486
+ }).execute();
487
+ }
488
+ }
489
+ chunkUSNT2KNT_cjs.__name(syncProductVariantsStatusWithProduct, "syncProductVariantsStatusWithProduct");
490
+ function coerceVariantStatusForProduct(variantStatus, productStatus, opts) {
491
+ const next = String(variantStatus ?? "draft").trim() || "draft";
492
+ const productLive = String(productStatus ?? "draft") === "available";
493
+ const approvalOk = !opts?.requireApproval || String(opts?.approvalStatus ?? "") === "approved";
494
+ if (!productLive || !approvalOk) {
495
+ return "draft";
496
+ }
497
+ return next;
498
+ }
499
+ chunkUSNT2KNT_cjs.__name(coerceVariantStatusForProduct, "coerceVariantStatusForProduct");
465
500
 
466
501
  // src/lib/event-approval.ts
467
502
  var EVENT_APPROVAL_STATUSES = [
@@ -4604,6 +4639,26 @@ function createCrudHandler(dataSource, entityMap, options) {
4604
4639
  }
4605
4640
  }
4606
4641
  }
4642
+ if (resource === "product_variants" && entityMap.products) {
4643
+ const productIdRaw = persistBody.productId;
4644
+ const productId = typeof productIdRaw === "number" ? productIdRaw : typeof productIdRaw === "string" && /^\d+$/.test(productIdRaw) ? parseInt(productIdRaw, 10) : NaN;
4645
+ if (Number.isFinite(productId)) {
4646
+ const parent = await dataSource.getRepository(entityMap.products).findOne({
4647
+ where: {
4648
+ id: productId
4649
+ }
4650
+ });
4651
+ const parentStatus = parent?.status;
4652
+ const parentApproval = parent?.approvalStatus;
4653
+ const requireApproval = await getRequireProductApproval(dataSource);
4654
+ persistBody.status = coerceVariantStatusForProduct(persistBody.status, parentStatus, {
4655
+ approvalStatus: parentApproval,
4656
+ requireApproval
4657
+ });
4658
+ } else if (!("status" in persistBody)) {
4659
+ persistBody.status = "draft";
4660
+ }
4661
+ }
4607
4662
  if (resource === "products") {
4608
4663
  const scopeForProduct = await resolveScope();
4609
4664
  const productFlags = await getVendorCatalogCreateFlags(dataSource);
@@ -6243,6 +6298,34 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6243
6298
  delete u.parentId;
6244
6299
  }
6245
6300
  }
6301
+ if (resource === "product_variants" && entityMap.products) {
6302
+ const currentVariant = await repo.findOne({
6303
+ where: {
6304
+ id: numericId
6305
+ }
6306
+ });
6307
+ if (!currentVariant) return json({
6308
+ message: "Not found"
6309
+ }, {
6310
+ status: 404
6311
+ });
6312
+ const productId = "productId" in updatePayload && updatePayload.productId != null ? Number(updatePayload.productId) : Number(currentVariant.productId);
6313
+ if (Number.isFinite(productId)) {
6314
+ const parent = await dataSource.getRepository(entityMap.products).findOne({
6315
+ where: {
6316
+ id: productId
6317
+ }
6318
+ });
6319
+ const parentStatus = parent?.status;
6320
+ const parentApproval = parent?.approvalStatus;
6321
+ const requireApproval = await getRequireProductApproval(dataSource);
6322
+ const nextStatus = "status" in updatePayload ? updatePayload.status : currentVariant.status;
6323
+ updatePayload.status = coerceVariantStatusForProduct(nextStatus, parentStatus, {
6324
+ approvalStatus: parentApproval,
6325
+ requireApproval
6326
+ });
6327
+ }
6328
+ }
6246
6329
  if (resource === "products") {
6247
6330
  const currentRow = await repo.findOne({
6248
6331
  where: {
@@ -6598,6 +6681,10 @@ function createCrudByIdHandler(dataSource, entityMap, options) {
6598
6681
  if (reloaded) updated = reloaded;
6599
6682
  }
6600
6683
  updated = hydrateProductDisplayName(updated);
6684
+ const productStatus = String(updated.status ?? "draft");
6685
+ if (Number.isFinite(updatedId) && updatedId > 0) {
6686
+ await syncProductVariantsStatusWithProduct(dataSource, entityMap, updatedId, productStatus);
6687
+ }
6601
6688
  if (getCms) {
6602
6689
  const cms = await getCms();
6603
6690
  await queueErpProductUpsertIfEnabled(cms, dataSource, entityMap, updated);
@@ -29815,7 +29902,8 @@ function createStorefrontApiHandler(config) {
29815
29902
  const repo = dataSource.getRepository(entityMap.product_variants);
29816
29903
  const rows = await repo.find({
29817
29904
  where: {
29818
- productId
29905
+ productId,
29906
+ status: "available"
29819
29907
  },
29820
29908
  order: {
29821
29909
  id: "ASC"
@@ -29848,6 +29936,52 @@ function createStorefrontApiHandler(config) {
29848
29936
  return result;
29849
29937
  }
29850
29938
  chunkUSNT2KNT_cjs.__name(loadProductVariantsWithPricing, "loadProductVariantsWithPricing");
29939
+ async function loadApplicableVendorPolicies(productId, vendorId) {
29940
+ if (!entityMap.refund_policies) return [];
29941
+ const policyRepo = dataSource.getRepository(entityMap.refund_policies);
29942
+ const format = /* @__PURE__ */ chunkUSNT2KNT_cjs.__name((row) => ({
29943
+ id: Number(row.id),
29944
+ name: String(row.name ?? "").trim(),
29945
+ desc: row.desc != null ? String(row.desc) : null,
29946
+ refundWindowDays: Number(row.refundWindowDays) || 0,
29947
+ type: String(row.type ?? "percentage"),
29948
+ value: Number(row.value) || 0
29949
+ }), "format");
29950
+ const linkedIds = /* @__PURE__ */ new Set();
29951
+ if (entityMap.product_config) {
29952
+ const configs = await dataSource.getRepository(entityMap.product_config).find({
29953
+ where: {
29954
+ productId
29955
+ }
29956
+ });
29957
+ for (const c of configs) {
29958
+ const id = Number(c.refundPolicyId);
29959
+ if (Number.isFinite(id) && id > 0) linkedIds.add(id);
29960
+ }
29961
+ }
29962
+ if (linkedIds.size > 0) {
29963
+ const rows = await policyRepo.find({
29964
+ where: {
29965
+ id: typeorm.In([
29966
+ ...linkedIds
29967
+ ])
29968
+ }
29969
+ });
29970
+ return rows.map((r) => format(r)).filter((p) => p.name).sort((a, b) => a.id - b.id);
29971
+ }
29972
+ if (vendorId == null || !Number.isFinite(Number(vendorId))) return [];
29973
+ const vendorRows = await policyRepo.find({
29974
+ where: {
29975
+ vendorId: Number(vendorId),
29976
+ status: "active"
29977
+ },
29978
+ order: {
29979
+ id: "ASC"
29980
+ }
29981
+ });
29982
+ return vendorRows.map((r) => format(r)).filter((p) => p.name);
29983
+ }
29984
+ chunkUSNT2KNT_cjs.__name(loadApplicableVendorPolicies, "loadApplicableVendorPolicies");
29851
29985
  return {
29852
29986
  async handle(method, path2, req) {
29853
29987
  try {
@@ -29915,13 +30049,11 @@ function createStorefrontApiHandler(config) {
29915
30049
  const url = new URL(req.url || "", "http://localhost");
29916
30050
  const collectionSlug = url.searchParams.get("collection")?.trim();
29917
30051
  const collectionId = url.searchParams.get("collectionId");
30052
+ const q = url.searchParams.get("q")?.trim() ?? "";
29918
30053
  const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20", 10)));
29919
30054
  const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
29920
- const where = {
29921
- status: "available",
29922
- deleted: false
29923
- };
29924
30055
  let collectionFilter = null;
30056
+ let collectionIdFilter = null;
29925
30057
  if (collectionSlug) {
29926
30058
  let col = null;
29927
30059
  if (/^\d+$/.test(collectionSlug)) {
@@ -29948,30 +30080,59 @@ function createStorefrontApiHandler(config) {
29948
30080
  collection: null
29949
30081
  });
29950
30082
  }
29951
- where.collectionId = col.id;
30083
+ collectionIdFilter = Number(col.id);
29952
30084
  collectionFilter = {
29953
30085
  name: col.name,
29954
30086
  slug: col.slug
29955
30087
  };
29956
30088
  } else if (collectionId) {
29957
30089
  const cid = parseInt(collectionId, 10);
29958
- if (Number.isFinite(cid)) where.collectionId = cid;
30090
+ if (Number.isFinite(cid)) collectionIdFilter = cid;
30091
+ }
30092
+ let items = [];
30093
+ let total = 0;
30094
+ if (q) {
30095
+ const like = `%${q.replace(/[%_]/g, "\\$&")}%`;
30096
+ const qb = productRepo().createQueryBuilder("p").where("p.status = :status", {
30097
+ status: "available"
30098
+ }).andWhere("p.deleted = :del", {
30099
+ del: false
30100
+ }).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)`, {
30101
+ like
30102
+ }).orderBy("p.id", "ASC").take(limit).skip(offset);
30103
+ if (collectionIdFilter != null) {
30104
+ qb.andWhere("p.collectionId = :cid", {
30105
+ cid: collectionIdFilter
30106
+ });
30107
+ }
30108
+ [items, total] = await qb.getManyAndCount();
30109
+ } else {
30110
+ const where = {
30111
+ status: "available",
30112
+ deleted: false
30113
+ };
30114
+ if (collectionIdFilter != null) where.collectionId = collectionIdFilter;
30115
+ const result = await productRepo().findAndCount({
30116
+ where,
30117
+ order: {
30118
+ id: "ASC"
30119
+ },
30120
+ take: limit,
30121
+ skip: offset
30122
+ });
30123
+ items = result[0];
30124
+ total = result[1];
29959
30125
  }
29960
- const [items, total] = await productRepo().findAndCount({
29961
- where,
29962
- order: {
29963
- id: "ASC"
29964
- },
29965
- take: limit,
29966
- skip: offset
29967
- });
29968
30126
  const products = await Promise.all(items.map((item) => enrichProductPricing(item)));
29969
30127
  return json({
29970
30128
  products,
29971
30129
  total,
29972
30130
  ...collectionFilter && {
29973
30131
  collection: collectionFilter
29974
- }
30132
+ },
30133
+ ...q ? {
30134
+ q
30135
+ } : {}
29975
30136
  });
29976
30137
  }
29977
30138
  if (path2[0] === "products" && path2.length === 2 && method === "GET") {
@@ -30007,12 +30168,14 @@ function createStorefrontApiHandler(config) {
30007
30168
  const pricing = await resolveProductEventPricing(Number(p.id));
30008
30169
  const enriched = await enrichProductPricing(p, pricing);
30009
30170
  const variants = await loadProductVariantsWithPricing(Number(p.id), pricing);
30171
+ const policies = await loadApplicableVendorPolicies(Number(p.id), p.vendorId != null ? Number(p.vendorId) : null);
30010
30172
  return json({
30011
30173
  ...enriched,
30012
30174
  attributes: attributeTags,
30013
30175
  ...variants.length ? {
30014
30176
  variants
30015
- } : {}
30177
+ } : {},
30178
+ policies
30016
30179
  });
30017
30180
  }
30018
30181
  if (path2[0] === "collections" && path2.length === 1 && method === "GET") {
@@ -30824,24 +30987,61 @@ function createStorefrontApiHandler(config) {
30824
30987
  }, {
30825
30988
  status: 404
30826
30989
  });
30990
+ const rawVariantId = body.variantId ?? body.variant_id;
30991
+ const variantIdNum = rawVariantId != null && String(rawVariantId).trim() !== "" ? Number(rawVariantId) : NaN;
30992
+ const hasVariantId = Number.isFinite(variantIdNum) && variantIdNum > 0;
30993
+ const bodyMeta = body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? body.metadata : {};
30994
+ 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;
30995
+ const options = optionsRaw && Object.fromEntries(Object.entries(optionsRaw).map(([k, v]) => [
30996
+ String(k),
30997
+ String(v ?? "").trim()
30998
+ ]).filter(([, v]) => v.length > 0));
30999
+ const lineMetadata = {
31000
+ ...bodyMeta,
31001
+ ...hasVariantId ? {
31002
+ variantId: variantIdNum
31003
+ } : {},
31004
+ ...options && Object.keys(options).length ? {
31005
+ options
31006
+ } : {}
31007
+ };
31008
+ if (typeof bodyMeta.imageUrl === "string" && bodyMeta.imageUrl.trim()) {
31009
+ lineMetadata.imageUrl = bodyMeta.imageUrl.trim();
31010
+ }
31011
+ if (typeof bodyMeta.title === "string" && bodyMeta.title.trim()) {
31012
+ lineMetadata.title = bodyMeta.title.trim();
31013
+ }
31014
+ const metadataPayload = Object.keys(lineMetadata).length ? lineMetadata : null;
30827
31015
  const { cart, setCookie, err } = await getOrCreateCart(req);
30828
31016
  if (err) return err;
30829
31017
  const cartId = cart.id;
30830
- const existing = await cartItemRepo().findOne({
31018
+ const sameProductLines = await cartItemRepo().find({
30831
31019
  where: {
30832
31020
  cartId,
30833
31021
  productId
30834
31022
  }
30835
31023
  });
31024
+ const existing = sameProductLines.find((row) => {
31025
+ const m = row.metadata;
31026
+ const existingVid = m?.variantId ?? m?.variant_id;
31027
+ if (hasVariantId) {
31028
+ return Number(existingVid) === variantIdNum;
31029
+ }
31030
+ return existingVid == null || existingVid === "";
31031
+ });
30836
31032
  if (existing) {
30837
31033
  await cartItemRepo().update(existing.id, {
30838
- quantity: existing.quantity + quantity
31034
+ quantity: existing.quantity + quantity,
31035
+ ...metadataPayload ? {
31036
+ metadata: metadataPayload
31037
+ } : {}
30839
31038
  });
30840
31039
  } else {
30841
31040
  await cartItemRepo().save(cartItemRepo().create({
30842
31041
  cartId,
30843
31042
  productId,
30844
- quantity
31043
+ quantity,
31044
+ metadata: metadataPayload
30845
31045
  }));
30846
31046
  }
30847
31047
  await cartRepo().update(cartId, {
@@ -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);
@@ -29805,7 +29892,8 @@ function createStorefrontApiHandler(config) {
29805
29892
  const repo = dataSource.getRepository(entityMap.product_variants);
29806
29893
  const rows = await repo.find({
29807
29894
  where: {
29808
- productId
29895
+ productId,
29896
+ status: "available"
29809
29897
  },
29810
29898
  order: {
29811
29899
  id: "ASC"
@@ -29838,6 +29926,52 @@ function createStorefrontApiHandler(config) {
29838
29926
  return result;
29839
29927
  }
29840
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");
29841
29975
  return {
29842
29976
  async handle(method, path2, req) {
29843
29977
  try {
@@ -29905,13 +30039,11 @@ function createStorefrontApiHandler(config) {
29905
30039
  const url = new URL(req.url || "", "http://localhost");
29906
30040
  const collectionSlug = url.searchParams.get("collection")?.trim();
29907
30041
  const collectionId = url.searchParams.get("collectionId");
30042
+ const q = url.searchParams.get("q")?.trim() ?? "";
29908
30043
  const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20", 10)));
29909
30044
  const offset = Math.max(0, parseInt(url.searchParams.get("offset") || "0", 10));
29910
- const where = {
29911
- status: "available",
29912
- deleted: false
29913
- };
29914
30045
  let collectionFilter = null;
30046
+ let collectionIdFilter = null;
29915
30047
  if (collectionSlug) {
29916
30048
  let col = null;
29917
30049
  if (/^\d+$/.test(collectionSlug)) {
@@ -29938,30 +30070,59 @@ function createStorefrontApiHandler(config) {
29938
30070
  collection: null
29939
30071
  });
29940
30072
  }
29941
- where.collectionId = col.id;
30073
+ collectionIdFilter = Number(col.id);
29942
30074
  collectionFilter = {
29943
30075
  name: col.name,
29944
30076
  slug: col.slug
29945
30077
  };
29946
30078
  } else if (collectionId) {
29947
30079
  const cid = parseInt(collectionId, 10);
29948
- 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];
29949
30115
  }
29950
- const [items, total] = await productRepo().findAndCount({
29951
- where,
29952
- order: {
29953
- id: "ASC"
29954
- },
29955
- take: limit,
29956
- skip: offset
29957
- });
29958
30116
  const products = await Promise.all(items.map((item) => enrichProductPricing(item)));
29959
30117
  return json({
29960
30118
  products,
29961
30119
  total,
29962
30120
  ...collectionFilter && {
29963
30121
  collection: collectionFilter
29964
- }
30122
+ },
30123
+ ...q ? {
30124
+ q
30125
+ } : {}
29965
30126
  });
29966
30127
  }
29967
30128
  if (path2[0] === "products" && path2.length === 2 && method === "GET") {
@@ -29997,12 +30158,14 @@ function createStorefrontApiHandler(config) {
29997
30158
  const pricing = await resolveProductEventPricing(Number(p.id));
29998
30159
  const enriched = await enrichProductPricing(p, pricing);
29999
30160
  const variants = await loadProductVariantsWithPricing(Number(p.id), pricing);
30161
+ const policies = await loadApplicableVendorPolicies(Number(p.id), p.vendorId != null ? Number(p.vendorId) : null);
30000
30162
  return json({
30001
30163
  ...enriched,
30002
30164
  attributes: attributeTags,
30003
30165
  ...variants.length ? {
30004
30166
  variants
30005
- } : {}
30167
+ } : {},
30168
+ policies
30006
30169
  });
30007
30170
  }
30008
30171
  if (path2[0] === "collections" && path2.length === 1 && method === "GET") {
@@ -30814,24 +30977,61 @@ function createStorefrontApiHandler(config) {
30814
30977
  }, {
30815
30978
  status: 404
30816
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;
30817
31005
  const { cart, setCookie, err } = await getOrCreateCart(req);
30818
31006
  if (err) return err;
30819
31007
  const cartId = cart.id;
30820
- const existing = await cartItemRepo().findOne({
31008
+ const sameProductLines = await cartItemRepo().find({
30821
31009
  where: {
30822
31010
  cartId,
30823
31011
  productId
30824
31012
  }
30825
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
+ });
30826
31022
  if (existing) {
30827
31023
  await cartItemRepo().update(existing.id, {
30828
- quantity: existing.quantity + quantity
31024
+ quantity: existing.quantity + quantity,
31025
+ ...metadataPayload ? {
31026
+ metadata: metadataPayload
31027
+ } : {}
30829
31028
  });
30830
31029
  } else {
30831
31030
  await cartItemRepo().save(cartItemRepo().create({
30832
31031
  cartId,
30833
31032
  productId,
30834
- quantity
31033
+ quantity,
31034
+ metadata: metadataPayload
30835
31035
  }));
30836
31036
  }
30837
31037
  await cartRepo().update(cartId, {