@lyxa.ai/marketing 1.0.37 → 1.0.40

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.
Files changed (59) hide show
  1. package/dist/lib/index.d.ts +87 -7
  2. package/dist/lib/index.d.ts.map +1 -1
  3. package/dist/lib/modules/coupon/services/coupon.service.d.ts +3 -0
  4. package/dist/lib/modules/coupon/services/coupon.service.d.ts.map +1 -1
  5. package/dist/lib/modules/coupon/services/coupon.service.js +122 -5
  6. package/dist/lib/modules/coupon/services/coupon.service.js.map +1 -1
  7. package/dist/lib/modules/coupon/services/patterns/repository/CouponRepository.d.ts.map +1 -1
  8. package/dist/lib/modules/coupon/services/patterns/repository/CouponRepository.js +7 -0
  9. package/dist/lib/modules/coupon/services/patterns/repository/CouponRepository.js.map +1 -1
  10. package/dist/lib/modules/coupon/validations/coupon.validation.d.ts.map +1 -1
  11. package/dist/lib/modules/coupon/validations/coupon.validation.js +5 -14
  12. package/dist/lib/modules/coupon/validations/coupon.validation.js.map +1 -1
  13. package/dist/lib/modules/marketing/patterns/repository/MarketingMetricsRepository.d.ts +39 -0
  14. package/dist/lib/modules/marketing/patterns/repository/MarketingMetricsRepository.d.ts.map +1 -0
  15. package/dist/lib/modules/marketing/patterns/repository/MarketingMetricsRepository.js +539 -0
  16. package/dist/lib/modules/marketing/patterns/repository/MarketingMetricsRepository.js.map +1 -0
  17. package/dist/lib/modules/marketing/routers/marketing.router.d.ts +87 -7
  18. package/dist/lib/modules/marketing/routers/marketing.router.d.ts.map +1 -1
  19. package/dist/lib/modules/marketing/routers/marketing.router.js +27 -8
  20. package/dist/lib/modules/marketing/routers/marketing.router.js.map +1 -1
  21. package/dist/lib/modules/marketing/services/marketing.service.d.ts +4 -8
  22. package/dist/lib/modules/marketing/services/marketing.service.d.ts.map +1 -1
  23. package/dist/lib/modules/marketing/services/marketing.service.js +65 -355
  24. package/dist/lib/modules/marketing/services/marketing.service.js.map +1 -1
  25. package/dist/lib/modules/marketing/services/product-marketing.service.d.ts +1 -0
  26. package/dist/lib/modules/marketing/services/product-marketing.service.d.ts.map +1 -1
  27. package/dist/lib/modules/marketing/services/product-marketing.service.js +4 -0
  28. package/dist/lib/modules/marketing/services/product-marketing.service.js.map +1 -1
  29. package/dist/lib/modules/marketing/validations/marketing.validation.d.ts +106 -1
  30. package/dist/lib/modules/marketing/validations/marketing.validation.d.ts.map +1 -1
  31. package/dist/lib/modules/marketing/validations/marketing.validation.js +31 -1
  32. package/dist/lib/modules/marketing/validations/marketing.validation.js.map +1 -1
  33. package/dist/lib/modules/punch-marketing-history/services/punch-marketing-history.service.d.ts.map +1 -1
  34. package/dist/lib/modules/punch-marketing-history/services/punch-marketing-history.service.js +19 -1
  35. package/dist/lib/modules/punch-marketing-history/services/punch-marketing-history.service.js.map +1 -1
  36. package/dist/lib/utility/utils.d.ts +3 -0
  37. package/dist/lib/utility/utils.d.ts.map +1 -0
  38. package/dist/lib/utility/utils.js +31 -0
  39. package/dist/lib/utility/utils.js.map +1 -0
  40. package/dist/types/index.d.ts +87 -7
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/modules/coupon/services/coupon.service.d.ts +3 -0
  43. package/dist/types/modules/coupon/services/coupon.service.d.ts.map +1 -1
  44. package/dist/types/modules/coupon/services/patterns/repository/CouponRepository.d.ts.map +1 -1
  45. package/dist/types/modules/coupon/validations/coupon.validation.d.ts.map +1 -1
  46. package/dist/types/modules/marketing/patterns/repository/MarketingMetricsRepository.d.ts +39 -0
  47. package/dist/types/modules/marketing/patterns/repository/MarketingMetricsRepository.d.ts.map +1 -0
  48. package/dist/types/modules/marketing/routers/marketing.router.d.ts +87 -7
  49. package/dist/types/modules/marketing/routers/marketing.router.d.ts.map +1 -1
  50. package/dist/types/modules/marketing/services/marketing.service.d.ts +4 -8
  51. package/dist/types/modules/marketing/services/marketing.service.d.ts.map +1 -1
  52. package/dist/types/modules/marketing/services/product-marketing.service.d.ts +1 -0
  53. package/dist/types/modules/marketing/services/product-marketing.service.d.ts.map +1 -1
  54. package/dist/types/modules/marketing/validations/marketing.validation.d.ts +106 -1
  55. package/dist/types/modules/marketing/validations/marketing.validation.d.ts.map +1 -1
  56. package/dist/types/modules/punch-marketing-history/services/punch-marketing-history.service.d.ts.map +1 -1
  57. package/dist/types/utility/utils.d.ts +15 -0
  58. package/dist/types/utility/utils.d.ts.map +1 -0
  59. package/package.json +2 -2
@@ -56,14 +56,17 @@ const _1 = require(".");
56
56
  const enum_2 = require("@lyxa.ai/core/dist/utilities/enum");
57
57
  const error_common_1 = require("@lyxa.ai/core/dist/utilities/error-common");
58
58
  const mongo_stream_changes_batch_process_shops_event_1 = require("@lyxa.ai/core/dist/libraries/event/events/mongo-stream-changes-batch-process-shops-event");
59
+ const MarketingMetricsRepository_1 = require("../patterns/repository/MarketingMetricsRepository");
59
60
  let MarketingService = class MarketingService {
60
61
  model;
61
62
  shopModel;
62
63
  orderModel;
64
+ marketingMetricsRepository;
63
65
  constructor() {
64
66
  this.model = models_1.MarketingModel;
65
67
  this.shopModel = models_1.ShopModel;
66
68
  this.orderModel = models_1.RegularOrderModel;
69
+ this.marketingMetricsRepository = MarketingMetricsRepository_1.marketingMetricsRepository;
67
70
  }
68
71
  async create(data, admin) {
69
72
  const now = (0, dayjs_1.dayjs)().toDate();
@@ -358,333 +361,6 @@ let MarketingService = class MarketingService {
358
361
  }
359
362
  return 'Product handled successfully';
360
363
  }
361
- async evaluateMarketingSpendLimits(marketings) {
362
- this.evaluateItemBasedMarketings(marketings);
363
- this.evalulateFreeDeliveryMarketings(marketings);
364
- return 'Evaluated spend limits';
365
- }
366
- async evaluateItemBasedMarketings(marketings) {
367
- const pipeline = [
368
- {
369
- $match: {
370
- $and: [{ marketings: { $exists: true } }, { marketings: { $ne: null } }],
371
- status: { $ne: enum_1.RegularOrderStatus.CANCELLED },
372
- $expr: {
373
- $gt: [
374
- {
375
- $size: {
376
- $setIntersection: ['$marketings', marketings],
377
- },
378
- },
379
- 0,
380
- ],
381
- },
382
- },
383
- },
384
- {
385
- $lookup: {
386
- from: 'lineItems',
387
- localField: 'cart',
388
- foreignField: 'cart',
389
- as: 'lineItems',
390
- },
391
- },
392
- {
393
- $unwind: '$lineItems',
394
- },
395
- {
396
- $lookup: {
397
- from: 'marketings',
398
- localField: 'lineItems.marketings',
399
- foreignField: '_id',
400
- as: 'marketing',
401
- },
402
- },
403
- {
404
- $unwind: '$marketing',
405
- },
406
- {
407
- $group: {
408
- _id: '$marketing._id',
409
- marketingType: { $first: '$marketing.marketingType' },
410
- spendLimit: { $first: '$marketing.spendLimit' },
411
- isSpendLimitEnabled: { $first: '$marketing.isSpendLimitEnabled' },
412
- totalCompanyDiscount: {
413
- $sum: {
414
- $cond: [
415
- { $eq: ['$marketing.marketingType', enum_2.MarketingType.DISCOUNT] },
416
- '$lineItems.calculationSnap.itemDiscountByCompany',
417
- 0,
418
- ],
419
- },
420
- },
421
- totalShopDiscount: {
422
- $sum: {
423
- $cond: [
424
- { $eq: ['$marketing.marketingType', enum_2.MarketingType.DISCOUNT] },
425
- '$lineItems.calculationSnap.itemDiscountByShop',
426
- 0,
427
- ],
428
- },
429
- },
430
- totalB1G1Company: {
431
- $sum: {
432
- $cond: [
433
- { $eq: ['$marketing.marketingType', enum_2.MarketingType.BUY1GET1] },
434
- '$lineItems.calculationSnap.b1g1DiscountByCompany',
435
- 0,
436
- ],
437
- },
438
- },
439
- totalB1G1Shop: {
440
- $sum: {
441
- $cond: [
442
- { $eq: ['$marketing.marketingType', enum_2.MarketingType.BUY1GET1] },
443
- '$lineItems.calculationSnap.b1g1DiscountByShop',
444
- 0,
445
- ],
446
- },
447
- },
448
- },
449
- },
450
- ];
451
- const result = await this.orderModel.aggregate(pipeline);
452
- const promises = [];
453
- result?.forEach(async (marketing) => {
454
- if (marketing.isSpendLimitEnabled) {
455
- const totalSpent = marketing.totalCompanyDiscount +
456
- marketing.totalShopDiscount +
457
- marketing.totalB1G1Company +
458
- marketing.totalB1G1Shop;
459
- if (totalSpent >= marketing.spendLimit) {
460
- await this.model.findByIdAndUpdate(marketing._id, {
461
- status: enum_1.MarketingStatus.EXPIRED,
462
- });
463
- promises.push(_1.productMarketingService.syncProductMarketingInfo(marketing._id));
464
- }
465
- }
466
- });
467
- await Promise.all(promises);
468
- }
469
- async evalulateFreeDeliveryMarketings(marketings) {
470
- const pipeline = [
471
- {
472
- $match: {
473
- $and: [{ marketings: { $exists: true } }, { marketings: { $ne: null } }],
474
- status: { $ne: enum_1.RegularOrderStatus.CANCELLED },
475
- $expr: {
476
- $gt: [
477
- {
478
- $size: {
479
- $setIntersection: ['$marketings', marketings],
480
- },
481
- },
482
- 0,
483
- ],
484
- },
485
- },
486
- },
487
- {
488
- $lookup: {
489
- from: 'marketings',
490
- localField: 'marketings',
491
- foreignField: '_id',
492
- as: 'marketing',
493
- },
494
- },
495
- {
496
- $unwind: '$marketing',
497
- },
498
- {
499
- $match: {
500
- 'marketing.marketingType': enum_2.MarketingType.FREE_DELIVERY,
501
- },
502
- },
503
- {
504
- $group: {
505
- _id: '$marketing._id',
506
- marketingType: { $first: '$marketing.marketingType' },
507
- spendLimit: { $first: '$marketing.spendLimit' },
508
- isSpendLimitEnabled: { $first: '$marketing.isSpendLimitEnabled' },
509
- totalFreeDeliveryCompany: {
510
- $sum: {
511
- $cond: [
512
- { $eq: ['$marketing.marketingType', enum_2.MarketingType.FREE_DELIVERY] },
513
- '$adjustedFinance.freeDelivery.companyCut',
514
- 0,
515
- ],
516
- },
517
- },
518
- totalFreeDeliveryShop: {
519
- $sum: {
520
- $cond: [
521
- { $eq: ['$marketing.marketingType', enum_2.MarketingType.FREE_DELIVERY] },
522
- '$adjustedFinance.freeDelivery.shopCut',
523
- 0,
524
- ],
525
- },
526
- },
527
- },
528
- },
529
- ];
530
- const result = await this.orderModel.aggregate(pipeline);
531
- const promises = [];
532
- result?.forEach(marketing => {
533
- if (marketing.isSpendLimitEnabled) {
534
- const totalSpent = marketing.totalFreeDeliveryCompany + marketing.totalFreeDeliveryShop;
535
- if (totalSpent >= marketing.spendLimit) {
536
- promises.push(this.model.findByIdAndUpdate(marketing._id, {
537
- status: enum_1.MarketingStatus.EXPIRED,
538
- }));
539
- }
540
- }
541
- });
542
- await Promise.all(promises);
543
- }
544
- async calculateMarketingSpent(marketingId) {
545
- const marketing = await this.model.findById(marketingId).lean();
546
- if (!marketing) {
547
- throw new Error('Marketing not found');
548
- }
549
- let amount = 0;
550
- switch (marketing.marketingType) {
551
- case enum_2.MarketingType.FREE_DELIVERY:
552
- amount = await this.calculateFreeDeliverySpent(marketingId);
553
- break;
554
- case enum_2.MarketingType.DISCOUNT:
555
- case enum_2.MarketingType.BUY1GET1:
556
- amount = await this.calculateItemBasedSpent(marketingId);
557
- break;
558
- case enum_2.MarketingType.PUNCH_MARKETING:
559
- amount = await this.calculatePunchMarketingSpent(marketingId);
560
- break;
561
- default:
562
- amount = 0;
563
- }
564
- return {
565
- amount,
566
- marketingType: marketing.marketingType,
567
- };
568
- }
569
- async calculateItemBasedSpent(marketingId) {
570
- const pipeline = [
571
- {
572
- $match: {
573
- $and: [{ marketings: { $exists: true } }, { marketings: { $ne: null } }],
574
- status: { $ne: enum_1.RegularOrderStatus.CANCELLED },
575
- marketings: marketingId,
576
- },
577
- },
578
- {
579
- $lookup: {
580
- from: 'lineItems',
581
- localField: 'cart',
582
- foreignField: 'cart',
583
- as: 'lineItems',
584
- },
585
- },
586
- { $unwind: '$lineItems' },
587
- {
588
- $lookup: {
589
- from: 'marketings',
590
- localField: 'lineItems.marketings',
591
- foreignField: '_id',
592
- as: 'marketing',
593
- },
594
- },
595
- { $unwind: '$marketing' },
596
- {
597
- $match: {
598
- 'marketing._id': marketingId,
599
- },
600
- },
601
- {
602
- $group: {
603
- _id: null,
604
- totalCompanyDiscount: {
605
- $sum: '$lineItems.calculationSnap.itemDiscountByCompany',
606
- },
607
- totalShopDiscount: {
608
- $sum: '$lineItems.calculationSnap.itemDiscountByShop',
609
- },
610
- totalB1G1Company: {
611
- $sum: '$lineItems.calculationSnap.b1g1DiscountByCompany',
612
- },
613
- totalB1G1Shop: {
614
- $sum: '$lineItems.calculationSnap.b1g1DiscountByShop',
615
- },
616
- },
617
- },
618
- ];
619
- const [result] = await this.orderModel.aggregate(pipeline);
620
- if (!result)
621
- return 0;
622
- return (result.totalCompanyDiscount + result.totalShopDiscount + result.totalB1G1Company + result.totalB1G1Shop);
623
- }
624
- async calculateFreeDeliverySpent(marketingId) {
625
- const pipeline = [
626
- {
627
- $match: {
628
- $and: [{ marketings: { $exists: true } }, { marketings: { $ne: null } }],
629
- status: { $ne: enum_1.RegularOrderStatus.CANCELLED },
630
- marketings: marketingId,
631
- },
632
- },
633
- {
634
- $lookup: {
635
- from: 'marketings',
636
- localField: 'marketings',
637
- foreignField: '_id',
638
- as: 'marketing',
639
- },
640
- },
641
- { $unwind: '$marketing' },
642
- {
643
- $match: {
644
- 'marketing._id': marketingId,
645
- 'marketing.marketingType': enum_2.MarketingType.FREE_DELIVERY,
646
- },
647
- },
648
- {
649
- $group: {
650
- _id: null,
651
- totalFreeDeliveryCompany: {
652
- $sum: '$adjustedFinance.freeDelivery.companyCut',
653
- },
654
- totalFreeDeliveryShop: {
655
- $sum: '$adjustedFinance.freeDelivery.shopCut',
656
- },
657
- },
658
- },
659
- ];
660
- const [result] = await this.orderModel.aggregate(pipeline);
661
- if (!result)
662
- return 0;
663
- return result.totalFreeDeliveryCompany + result.totalFreeDeliveryShop;
664
- }
665
- async calculatePunchMarketingSpent(marketingId) {
666
- const pipeline = [
667
- {
668
- $match: {
669
- $and: [{ marketings: { $exists: true } }, { marketings: { $ne: null } }],
670
- status: { $ne: enum_1.RegularOrderStatus.CANCELLED },
671
- marketings: marketingId,
672
- },
673
- },
674
- {
675
- $group: {
676
- _id: null,
677
- totalPunchDiscount: {
678
- $sum: {
679
- $ifNull: ['$adjustedFinance.pricing.punchMarketingDiscount', 0],
680
- },
681
- },
682
- },
683
- },
684
- ];
685
- const [result] = await this.orderModel.aggregate(pipeline);
686
- return result?.totalPunchDiscount ?? 0;
687
- }
688
364
  async incrementMarketingSpentFromOrder(orderId) {
689
365
  const order = (await this.orderModel
690
366
  .findById(orderId)
@@ -692,7 +368,7 @@ let MarketingService = class MarketingService {
692
368
  if (!order || !order.marketings?.length) {
693
369
  return;
694
370
  }
695
- const marketingSpendMap = this.calculateMarketingSpendPerOrder(order);
371
+ const marketingSpendMap = this.marketingMetricsRepository.calculateMarketingSpendPerOrder(order);
696
372
  const bulkOps = [...marketingSpendMap.entries()].map(([marketingId, amountSpent]) => ({
697
373
  updateOne: {
698
374
  filter: { _id: marketingId },
@@ -702,44 +378,78 @@ let MarketingService = class MarketingService {
702
378
  if (bulkOps.length) {
703
379
  await this.model.bulkWrite(bulkOps);
704
380
  }
705
- await this.expireMarketingsExceedingSpendLimits();
381
+ await this.expireMarketingsExceedingSpendLimits(Array.from(marketingSpendMap.keys()));
706
382
  return 'Marketing amount spent incremented successfully';
707
383
  }
708
- async expireMarketingsExceedingSpendLimits() {
384
+ async decrementMarketingSpentFromOrder(orderId) {
385
+ const order = (await this.orderModel
386
+ .findById(orderId)
387
+ .populate('lineItems'));
388
+ if (!order || !order.marketings?.length) {
389
+ return;
390
+ }
391
+ const marketingSpendMap = this.marketingMetricsRepository.calculateMarketingSpendPerOrder(order);
392
+ const bulkOps = [...marketingSpendMap.entries()].map(([marketingId, amountSpent]) => ({
393
+ updateOne: {
394
+ filter: { _id: marketingId },
395
+ update: { $inc: { amountSpent: -amountSpent } },
396
+ },
397
+ }));
398
+ if (bulkOps.length) {
399
+ await this.model.bulkWrite(bulkOps);
400
+ }
401
+ await this.unExpireMarketingsBelowSpendLimits(Array.from(marketingSpendMap.keys()));
402
+ return 'Marketing amount spent decremented successfully';
403
+ }
404
+ async expireMarketingsExceedingSpendLimits(marketingIds) {
709
405
  await this.model.updateMany({
406
+ _id: { $in: marketingIds },
710
407
  isSpendLimitEnabled: true,
711
408
  status: { $ne: enum_1.MarketingStatus.EXPIRED },
712
409
  $expr: { $gte: ['$amountSpent', '$spendLimit'] },
713
410
  }, { $set: { status: enum_1.MarketingStatus.EXPIRED } });
714
411
  }
715
- calculateMarketingSpendPerOrder(order) {
716
- const spendMap = new Map();
717
- const addSpend = (marketingId, amount) => {
718
- if (!amount || amount <= 0)
719
- return;
720
- spendMap.set(marketingId, (spendMap.get(marketingId) ?? 0) + amount);
721
- };
722
- for (const lineItem of order.lineItems ?? []) {
723
- for (const marketingId of lineItem.marketings ?? []) {
724
- const snap = lineItem.calculationSnap ?? {};
725
- addSpend(marketingId._id, (Number(snap.itemDiscountByCompany) || 0) +
726
- (Number(snap.itemDiscountByShop) || 0) +
727
- (Number(snap.b1g1DiscountByCompany) || 0) +
728
- (Number(snap.b1g1DiscountByShop) || 0));
729
- }
730
- }
731
- if (order.adjustedFinance?.freeDelivery) {
732
- for (const marketingId of order.marketings ?? []) {
733
- addSpend(marketingId._id, (order.adjustedFinance.freeDelivery.companyCut ?? 0) +
734
- (order.adjustedFinance.freeDelivery.shopCut ?? 0));
412
+ async unExpireMarketingsBelowSpendLimits(marketingIds) {
413
+ await this.model.updateMany({
414
+ _id: { $in: marketingIds },
415
+ isSpendLimitEnabled: true,
416
+ status: enum_1.MarketingStatus.EXPIRED,
417
+ $expr: { $lt: ['$amountSpent', '$spendLimit'] },
418
+ }, { $set: { status: enum_1.MarketingStatus.ACTIVE } });
419
+ }
420
+ async recomputeAndSyncAllMarketingSpent() {
421
+ console.time('Recomputing all marketing spend');
422
+ const cursor = this.model
423
+ .find({ marketingType: { $ne: enum_2.MarketingType.FEATURED } }, { _id: 1 })
424
+ .lean()
425
+ .cursor();
426
+ const bulkOps = [];
427
+ const marketingIds = [];
428
+ for await (const marketing of cursor) {
429
+ console.log(`Recomputing marketing spend for marketing ID: ${marketing._id}`);
430
+ const { amount } = await this.marketingMetricsRepository.calculateMarketingSpent(marketing._id);
431
+ bulkOps.push({
432
+ updateOne: {
433
+ filter: { _id: marketing._id },
434
+ update: {
435
+ $set: { amountSpent: amount },
436
+ },
437
+ },
438
+ });
439
+ marketingIds.push(marketing._id);
440
+ if (bulkOps.length >= 500) {
441
+ console.log(`Flushing 500 marketing spend updates...`);
442
+ await this.model.bulkWrite(bulkOps);
443
+ bulkOps.length = 0;
735
444
  }
736
445
  }
737
- if (order.adjustedFinance?.pricing?.punchMarketingDiscount) {
738
- for (const marketingId of order.marketings ?? []) {
739
- addSpend(marketingId._id, order.adjustedFinance.pricing.punchMarketingDiscount);
740
- }
446
+ if (bulkOps.length) {
447
+ console.log(`Flushing remaining ${bulkOps.length} marketing spend updates...`);
448
+ await this.model.bulkWrite(bulkOps);
741
449
  }
742
- return spendMap;
450
+ await this.expireMarketingsExceedingSpendLimits(marketingIds);
451
+ await this.unExpireMarketingsBelowSpendLimits(marketingIds);
452
+ console.timeEnd('Recomputing all marketing spend');
743
453
  }
744
454
  };
745
455
  exports.MarketingService = MarketingService;