@lyxa.ai/marketing 1.0.54 → 1.0.56

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 (104) hide show
  1. package/dist/lib/index.d.ts +1289 -0
  2. package/dist/lib/index.d.ts.map +1 -1
  3. package/dist/lib/index.js +1 -1
  4. package/dist/lib/index.js.map +1 -1
  5. package/dist/lib/modules/coupon/routers/coupon.router.d.ts +1289 -0
  6. package/dist/lib/modules/coupon/routers/coupon.router.d.ts.map +1 -1
  7. package/dist/lib/modules/coupon/routers/coupon.router.js +14 -0
  8. package/dist/lib/modules/coupon/routers/coupon.router.js.map +1 -1
  9. package/dist/lib/modules/coupon/services/coupon.service.d.ts +30 -2
  10. package/dist/lib/modules/coupon/services/coupon.service.d.ts.map +1 -1
  11. package/dist/lib/modules/coupon/services/coupon.service.js +34 -1
  12. package/dist/lib/modules/coupon/services/coupon.service.js.map +1 -1
  13. package/dist/lib/modules/coupon/services/custom-coupon.service.d.ts +5 -0
  14. package/dist/lib/modules/coupon/services/custom-coupon.service.d.ts.map +1 -1
  15. package/dist/lib/modules/coupon/services/helper.service.d.ts +5 -0
  16. package/dist/lib/modules/coupon/services/helper.service.d.ts.map +1 -1
  17. package/dist/lib/modules/coupon/services/individual-store-coupon.service.d.ts +5 -0
  18. package/dist/lib/modules/coupon/services/individual-store-coupon.service.d.ts.map +1 -1
  19. package/dist/lib/modules/coupon/services/patterns/repository/CouponMetricsRepository.d.ts +2 -0
  20. package/dist/lib/modules/coupon/services/patterns/repository/CouponMetricsRepository.d.ts.map +1 -1
  21. package/dist/lib/modules/coupon/services/patterns/repository/CouponMetricsRepository.js +77 -7
  22. package/dist/lib/modules/coupon/services/patterns/repository/CouponMetricsRepository.js.map +1 -1
  23. package/dist/lib/modules/coupon/services/patterns/repository/CouponRepository.d.ts +17 -0
  24. package/dist/lib/modules/coupon/services/patterns/repository/CouponRepository.d.ts.map +1 -1
  25. package/dist/lib/modules/coupon/services/patterns/repository/CouponRepository.js +135 -0
  26. package/dist/lib/modules/coupon/services/patterns/repository/CouponRepository.js.map +1 -1
  27. package/dist/lib/modules/coupon/services/shops-categories-coupon.service.d.ts +5 -0
  28. package/dist/lib/modules/coupon/services/shops-categories-coupon.service.d.ts.map +1 -1
  29. package/dist/lib/modules/coupon/validations/coupon.validation.d.ts +76 -0
  30. package/dist/lib/modules/coupon/validations/coupon.validation.d.ts.map +1 -1
  31. package/dist/lib/modules/coupon/validations/coupon.validation.js +27 -1
  32. package/dist/lib/modules/coupon/validations/coupon.validation.js.map +1 -1
  33. package/dist/lib/modules/coupon/validations/create-coupon.validation.d.ts.map +1 -1
  34. package/dist/lib/modules/coupon/validations/create-coupon.validation.js +4 -1
  35. package/dist/lib/modules/coupon/validations/create-coupon.validation.js.map +1 -1
  36. package/dist/lib/modules/limited-time-offer/config/limited-time-offer-config.validation.d.ts +9 -1
  37. package/dist/lib/modules/limited-time-offer/config/limited-time-offer-config.validation.d.ts.map +1 -1
  38. package/dist/lib/modules/limited-time-offer/config/limited-time-offer-config.validation.js +26 -3
  39. package/dist/lib/modules/limited-time-offer/config/limited-time-offer-config.validation.js.map +1 -1
  40. package/dist/lib/modules/tournament/match/events/evaluate-predictions.consumer.js +1 -1
  41. package/dist/lib/modules/tournament/match/events/evaluate-predictions.consumer.js.map +1 -1
  42. package/dist/lib/modules/tournament/notification/__tests__/notification.service.spec.d.ts +2 -0
  43. package/dist/lib/modules/tournament/notification/__tests__/notification.service.spec.d.ts.map +1 -0
  44. package/dist/lib/modules/tournament/notification/__tests__/notification.service.spec.js +266 -0
  45. package/dist/lib/modules/tournament/notification/__tests__/notification.service.spec.js.map +1 -0
  46. package/dist/lib/modules/tournament/notification/notification.service.d.ts +14 -3
  47. package/dist/lib/modules/tournament/notification/notification.service.d.ts.map +1 -1
  48. package/dist/lib/modules/tournament/notification/notification.service.js +89 -31
  49. package/dist/lib/modules/tournament/notification/notification.service.js.map +1 -1
  50. package/dist/lib/modules/tournament/participant/participant.repository.d.ts +1 -0
  51. package/dist/lib/modules/tournament/participant/participant.repository.d.ts.map +1 -1
  52. package/dist/lib/modules/tournament/participant/participant.repository.js +9 -0
  53. package/dist/lib/modules/tournament/participant/participant.repository.js.map +1 -1
  54. package/dist/lib/modules/tournament/participant/participant.service.d.ts +1 -0
  55. package/dist/lib/modules/tournament/participant/participant.service.d.ts.map +1 -1
  56. package/dist/lib/modules/tournament/participant/participant.service.js +6 -0
  57. package/dist/lib/modules/tournament/participant/participant.service.js.map +1 -1
  58. package/dist/lib/modules/tournament/reward/reward.builder.d.ts.map +1 -1
  59. package/dist/lib/modules/tournament/reward/reward.builder.js +3 -3
  60. package/dist/lib/modules/tournament/reward/reward.builder.js.map +1 -1
  61. package/dist/lib/modules/tournament/reward/reward.types.d.ts +1 -0
  62. package/dist/lib/modules/tournament/reward/reward.types.d.ts.map +1 -1
  63. package/dist/lib/modules/tournament/reward/reward.types.js.map +1 -1
  64. package/dist/lib/modules/tournament/schedule/schedule.service.d.ts +7 -0
  65. package/dist/lib/modules/tournament/schedule/schedule.service.d.ts.map +1 -1
  66. package/dist/lib/modules/tournament/schedule/schedule.service.js +52 -19
  67. package/dist/lib/modules/tournament/schedule/schedule.service.js.map +1 -1
  68. package/dist/lib/utility/utils.js +2 -2
  69. package/dist/lib/utility/utils.js.map +1 -1
  70. package/dist/types/index.d.ts +1289 -0
  71. package/dist/types/index.d.ts.map +1 -1
  72. package/dist/types/modules/coupon/routers/coupon.router.d.ts +1289 -0
  73. package/dist/types/modules/coupon/routers/coupon.router.d.ts.map +1 -1
  74. package/dist/types/modules/coupon/services/coupon.service.d.ts +30 -2
  75. package/dist/types/modules/coupon/services/coupon.service.d.ts.map +1 -1
  76. package/dist/types/modules/coupon/services/custom-coupon.service.d.ts +5 -0
  77. package/dist/types/modules/coupon/services/custom-coupon.service.d.ts.map +1 -1
  78. package/dist/types/modules/coupon/services/helper.service.d.ts +5 -0
  79. package/dist/types/modules/coupon/services/helper.service.d.ts.map +1 -1
  80. package/dist/types/modules/coupon/services/individual-store-coupon.service.d.ts +5 -0
  81. package/dist/types/modules/coupon/services/individual-store-coupon.service.d.ts.map +1 -1
  82. package/dist/types/modules/coupon/services/patterns/repository/CouponMetricsRepository.d.ts +2 -0
  83. package/dist/types/modules/coupon/services/patterns/repository/CouponMetricsRepository.d.ts.map +1 -1
  84. package/dist/types/modules/coupon/services/patterns/repository/CouponRepository.d.ts +17 -0
  85. package/dist/types/modules/coupon/services/patterns/repository/CouponRepository.d.ts.map +1 -1
  86. package/dist/types/modules/coupon/services/shops-categories-coupon.service.d.ts +5 -0
  87. package/dist/types/modules/coupon/services/shops-categories-coupon.service.d.ts.map +1 -1
  88. package/dist/types/modules/coupon/validations/coupon.validation.d.ts +76 -0
  89. package/dist/types/modules/coupon/validations/coupon.validation.d.ts.map +1 -1
  90. package/dist/types/modules/coupon/validations/create-coupon.validation.d.ts.map +1 -1
  91. package/dist/types/modules/limited-time-offer/config/limited-time-offer-config.validation.d.ts +9 -1
  92. package/dist/types/modules/limited-time-offer/config/limited-time-offer-config.validation.d.ts.map +1 -1
  93. package/dist/types/modules/tournament/notification/notification.service.d.ts +14 -3
  94. package/dist/types/modules/tournament/notification/notification.service.d.ts.map +1 -1
  95. package/dist/types/modules/tournament/participant/participant.repository.d.ts +1 -0
  96. package/dist/types/modules/tournament/participant/participant.repository.d.ts.map +1 -1
  97. package/dist/types/modules/tournament/participant/participant.service.d.ts +1 -0
  98. package/dist/types/modules/tournament/participant/participant.service.d.ts.map +1 -1
  99. package/dist/types/modules/tournament/reward/reward.builder.d.ts.map +1 -1
  100. package/dist/types/modules/tournament/reward/reward.types.d.ts +1 -0
  101. package/dist/types/modules/tournament/reward/reward.types.d.ts.map +1 -1
  102. package/dist/types/modules/tournament/schedule/schedule.service.d.ts +7 -0
  103. package/dist/types/modules/tournament/schedule/schedule.service.d.ts.map +1 -1
  104. package/package.json +2 -2
@@ -10,10 +10,33 @@ exports.CreateLimitedTimeOfferConfigSchema = zod_1.z.object({
10
10
  discountOptions: zod_1.z.array(global_validation_1.ZodValidation.number('Discount option')).default([]),
11
11
  maxDiscountCaps: zod_1.z.array(global_validation_1.ZodValidation.number('Max discount cap')).default([]),
12
12
  });
13
- exports.UpdateLimitedTimeOfferConfigSchema = zod_1.z.object({
13
+ exports.UpdateLimitedTimeOfferConfigSchema = zod_1.z
14
+ .object({
14
15
  _id: global_validation_1.ZodValidation.objectId(),
15
- discountOptions: zod_1.z.array(global_validation_1.ZodValidation.number('Discount option')).min(1, 'At least one discount option is required'),
16
- maxDiscountCaps: zod_1.z.array(global_validation_1.ZodValidation.number('Max discount cap')).min(1, 'At least one max discount cap is required'),
16
+ discountOptions: zod_1.z
17
+ .array(global_validation_1.ZodValidation.number('Discount option'))
18
+ .min(1, 'At least one discount option is required'),
19
+ maxDiscountCaps: zod_1.z
20
+ .array(global_validation_1.ZodValidation.number('Max discount cap'))
21
+ .min(1, 'At least one max discount cap is required'),
22
+ })
23
+ .superRefine((data, ctx) => {
24
+ const hasDuplicateDiscountOptions = new Set(data.discountOptions).size !== data.discountOptions.length;
25
+ if (hasDuplicateDiscountOptions) {
26
+ ctx.addIssue({
27
+ code: zod_1.z.ZodIssueCode.custom,
28
+ path: ['discountOptions'],
29
+ message: 'Duplicate discount options are not allowed',
30
+ });
31
+ }
32
+ const hasDuplicateMaxDiscountCaps = new Set(data.maxDiscountCaps).size !== data.maxDiscountCaps.length;
33
+ if (hasDuplicateMaxDiscountCaps) {
34
+ ctx.addIssue({
35
+ code: zod_1.z.ZodIssueCode.custom,
36
+ path: ['maxDiscountCaps'],
37
+ message: 'Duplicate max discount caps are not allowed',
38
+ });
39
+ }
17
40
  });
18
41
  exports.LimitedTimeOfferConfigSchema = exports.CreateLimitedTimeOfferConfigSchema.extend({
19
42
  _id: global_validation_1.ZodValidation.objectId(),
@@ -1 +1 @@
1
- {"version":3,"file":"limited-time-offer-config.validation.js","sourceRoot":"/","sources":["modules/limited-time-offer/config/limited-time-offer-config.validation.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,iGAA+F;AAC/F,4DAAkE;AAClE,wEAA+E;AAElE,QAAA,kCAAkC,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1D,YAAY,EAAE,iCAAa,CAAC,QAAQ,CAAC,oBAAa,EAAE,eAAe,CAAC;IACpE,eAAe,EAAE,OAAC,CAAC,KAAK,CAAC,iCAAa,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7E,eAAe,EAAE,OAAC,CAAC,KAAK,CAAC,iCAAa,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CAC9E,CAAC,CAAC;AAEU,QAAA,kCAAkC,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1D,GAAG,EAAE,iCAAa,CAAC,QAAQ,EAAE;IAC7B,eAAe,EAAE,OAAC,CAAC,KAAK,CAAC,iCAAa,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,0CAA0C,CAAC;IACpH,eAAe,EAAE,OAAC,CAAC,KAAK,CAAC,iCAAa,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,2CAA2C,CAAC;CACtH,CAAC,CAAC;AAEU,QAAA,4BAA4B,GAAG,0CAAkC,CAAC,MAAM,CAAC;IACrF,GAAG,EAAE,iCAAa,CAAC,QAAQ,EAAE;IAC7B,SAAS,EAAE,OAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACzC,SAAS,EAAE,OAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACzC,CAAC,CAAC;AAEU,QAAA,oCAAoC,GAAG,IAAA,iCAAoB,EAAC,oCAA4B,CAAC,CAAC","sourcesContent":["import { z } from 'zod';\nimport { ZodValidation, DTO } from '@lyxa.ai/core/dist/utilities/validation/global-validation';\nimport { MarketingType } from '@lyxa.ai/core/dist/utilities/enum';\nimport { createResponseSchema } from '@lyxa.ai/core/dist/utilities/validation';\n\nexport const CreateLimitedTimeOfferConfigSchema = z.object({\n\tcampaignType: ZodValidation.enumType(MarketingType, 'Campaign type'),\n\tdiscountOptions: z.array(ZodValidation.number('Discount option')).default([]),\n\tmaxDiscountCaps: z.array(ZodValidation.number('Max discount cap')).default([]),\n});\n\nexport const UpdateLimitedTimeOfferConfigSchema = z.object({\n\t_id: ZodValidation.objectId(),\n\tdiscountOptions: z.array(ZodValidation.number('Discount option')).min(1, 'At least one discount option is required'),\n\tmaxDiscountCaps: z.array(ZodValidation.number('Max discount cap')).min(1, 'At least one max discount cap is required'),\n});\n\nexport const LimitedTimeOfferConfigSchema = CreateLimitedTimeOfferConfigSchema.extend({\n\t_id: ZodValidation.objectId(),\n\tcreatedAt: z.date().nullable().optional(),\n\tupdatedAt: z.date().nullable().optional(),\n});\n\nexport const LimitedTimeOfferConfigResponseSchema = createResponseSchema(LimitedTimeOfferConfigSchema);\n\nexport type CreateLimitedTimeOfferConfigDTO = DTO<typeof CreateLimitedTimeOfferConfigSchema>;\nexport type UpdateLimitedTimeOfferConfigDTO = DTO<typeof UpdateLimitedTimeOfferConfigSchema>;\nexport type LimitedTimeOfferConfigDTO = DTO<typeof LimitedTimeOfferConfigSchema>;\n"]}
1
+ {"version":3,"file":"limited-time-offer-config.validation.js","sourceRoot":"/","sources":["modules/limited-time-offer/config/limited-time-offer-config.validation.ts"],"names":[],"mappings":";;;AAAA,6BAAwB;AACxB,iGAA+F;AAC/F,4DAAkE;AAClE,wEAA+E;AAElE,QAAA,kCAAkC,GAAG,OAAC,CAAC,MAAM,CAAC;IAC1D,YAAY,EAAE,iCAAa,CAAC,QAAQ,CAAC,oBAAa,EAAE,eAAe,CAAC;IACpE,eAAe,EAAE,OAAC,CAAC,KAAK,CAAC,iCAAa,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7E,eAAe,EAAE,OAAC,CAAC,KAAK,CAAC,iCAAa,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CAC9E,CAAC,CAAC;AAEU,QAAA,kCAAkC,GAAG,OAAC;KACjD,MAAM,CAAC;IACP,GAAG,EAAE,iCAAa,CAAC,QAAQ,EAAE;IAC7B,eAAe,EAAE,OAAC;SAChB,KAAK,CAAC,iCAAa,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;SAC9C,GAAG,CAAC,CAAC,EAAE,0CAA0C,CAAC;IACpD,eAAe,EAAE,OAAC;SAChB,KAAK,CAAC,iCAAa,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC;SAC/C,GAAG,CAAC,CAAC,EAAE,2CAA2C,CAAC;CACrD,CAAC;KACD,WAAW,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;IAC1B,MAAM,2BAA2B,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;IAEvG,IAAI,2BAA2B,EAAE,CAAC;QACjC,GAAG,CAAC,QAAQ,CAAC;YACZ,IAAI,EAAE,OAAC,CAAC,YAAY,CAAC,MAAM;YAC3B,IAAI,EAAE,CAAC,iBAAiB,CAAC;YACzB,OAAO,EAAE,4CAA4C;SACrD,CAAC,CAAC;IACJ,CAAC;IAED,MAAM,2BAA2B,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;IAEvG,IAAI,2BAA2B,EAAE,CAAC;QACjC,GAAG,CAAC,QAAQ,CAAC;YACZ,IAAI,EAAE,OAAC,CAAC,YAAY,CAAC,MAAM;YAC3B,IAAI,EAAE,CAAC,iBAAiB,CAAC;YACzB,OAAO,EAAE,6CAA6C;SACtD,CAAC,CAAC;IACJ,CAAC;AACF,CAAC,CAAC,CAAC;AAES,QAAA,4BAA4B,GAAG,0CAAkC,CAAC,MAAM,CAAC;IACrF,GAAG,EAAE,iCAAa,CAAC,QAAQ,EAAE;IAC7B,SAAS,EAAE,OAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACzC,SAAS,EAAE,OAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACzC,CAAC,CAAC;AAEU,QAAA,oCAAoC,GAAG,IAAA,iCAAoB,EAAC,oCAA4B,CAAC,CAAC","sourcesContent":["import { z } from 'zod';\nimport { ZodValidation, DTO } from '@lyxa.ai/core/dist/utilities/validation/global-validation';\nimport { MarketingType } from '@lyxa.ai/core/dist/utilities/enum';\nimport { createResponseSchema } from '@lyxa.ai/core/dist/utilities/validation';\n\nexport const CreateLimitedTimeOfferConfigSchema = z.object({\n\tcampaignType: ZodValidation.enumType(MarketingType, 'Campaign type'),\n\tdiscountOptions: z.array(ZodValidation.number('Discount option')).default([]),\n\tmaxDiscountCaps: z.array(ZodValidation.number('Max discount cap')).default([]),\n});\n\nexport const UpdateLimitedTimeOfferConfigSchema = z\n\t.object({\n\t\t_id: ZodValidation.objectId(),\n\t\tdiscountOptions: z\n\t\t\t.array(ZodValidation.number('Discount option'))\n\t\t\t.min(1, 'At least one discount option is required'),\n\t\tmaxDiscountCaps: z\n\t\t\t.array(ZodValidation.number('Max discount cap'))\n\t\t\t.min(1, 'At least one max discount cap is required'),\n\t})\n\t.superRefine((data, ctx) => {\n\t\tconst hasDuplicateDiscountOptions = new Set(data.discountOptions).size !== data.discountOptions.length;\n\n\t\tif (hasDuplicateDiscountOptions) {\n\t\t\tctx.addIssue({\n\t\t\t\tcode: z.ZodIssueCode.custom,\n\t\t\t\tpath: ['discountOptions'],\n\t\t\t\tmessage: 'Duplicate discount options are not allowed',\n\t\t\t});\n\t\t}\n\n\t\tconst hasDuplicateMaxDiscountCaps = new Set(data.maxDiscountCaps).size !== data.maxDiscountCaps.length;\n\n\t\tif (hasDuplicateMaxDiscountCaps) {\n\t\t\tctx.addIssue({\n\t\t\t\tcode: z.ZodIssueCode.custom,\n\t\t\t\tpath: ['maxDiscountCaps'],\n\t\t\t\tmessage: 'Duplicate max discount caps are not allowed',\n\t\t\t});\n\t\t}\n\t});\n\nexport const LimitedTimeOfferConfigSchema = CreateLimitedTimeOfferConfigSchema.extend({\n\t_id: ZodValidation.objectId(),\n\tcreatedAt: z.date().nullable().optional(),\n\tupdatedAt: z.date().nullable().optional(),\n});\n\nexport const LimitedTimeOfferConfigResponseSchema = createResponseSchema(LimitedTimeOfferConfigSchema);\n\nexport type CreateLimitedTimeOfferConfigDTO = DTO<typeof CreateLimitedTimeOfferConfigSchema>;\nexport type UpdateLimitedTimeOfferConfigDTO = DTO<typeof UpdateLimitedTimeOfferConfigSchema>;\nexport type LimitedTimeOfferConfigDTO = DTO<typeof LimitedTimeOfferConfigSchema>;\n"]}
@@ -48,7 +48,7 @@ async function EvaluatePredictionsConsumer() {
48
48
  data: { tournamentId: tournamentId.toString(), matchId: matchId.toString(), winnerCount: winnerIds.length, couponDetails },
49
49
  });
50
50
  await notification_service_1.tournamentNotificationService.notifyUsers({
51
- title: winner?.title ?? '🎉 Your prediction was currect.',
51
+ title: winner?.title ?? '🎉 Your prediction was correct.',
52
52
  body: winner?.body ?? 'Your coupon is now available',
53
53
  tournamentId: tournamentId.toString(),
54
54
  tournamentNotificationType: enum_1.TournamentNotificationTag.MATCH_RESULT_WINNER,
@@ -1 +1 @@
1
- {"version":3,"file":"evaluate-predictions.consumer.js","sourceRoot":"/","sources":["modules/tournament/match/events/evaluate-predictions.consumer.ts"],"names":[],"mappings":"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","sourcesContent":["import {mongoose} from '@typegoose/typegoose';\nimport {EvaluatePredictionsPayload, TOURNAMENT_EVALUATE_PREDICTIONS_EVENT} from './evaluate-predictions.event';\nimport {predictionService} from '../../prediction/prediction.service';\nimport {rewardService} from '../../reward/reward.service';\nimport {tournamentNotificationService} from '../../notification/notification.service';\nimport {notificationTemplateRepository} from '../../notification/notification-template.repository';\nimport {TournamentNotificationTag} from '@lyxa.ai/core/dist/utilities/enum';\nimport {RewardBuilder} from '../../reward/reward.builder';\nimport {CouponRewardResult, RewardType} from '../../reward/reward.types';\nimport {subscribeToEvent} from '@lyxa.ai/core/dist/libraries/event';\nimport {tournamentConfigRepository} from '@modules/tournament/config/config.repository';\nimport {notFoundError} from '@lyxa.ai/core/dist/utilities/error-common';\nimport {matchEvaluationOrchestrator} from \"@modules/tournament/match/events/match-evaluation.orchestrator\";\nimport {getLibraries} from '@lyxa.ai/core';\n\nexport async function EvaluatePredictionsConsumer() {\n await subscribeToEvent(TOURNAMENT_EVALUATE_PREDICTIONS_EVENT, async (payload: EvaluatePredictionsPayload) => {\n if (!payload.matches?.length) return;\n const tournamentId = new mongoose.Types.ObjectId(payload.tournamentId.toString());\n const logger = getLibraries().getLoggerKit();\n\n logger.info('Evaluate predictions event received', {\n tag: 'tournament.prediction.evaluate',\n data: { tournamentId: tournamentId.toString(), matchCount: payload.matches.length, matches: payload.matches },\n });\n\n const tournament = await tournamentConfigRepository.findById(tournamentId);\n if (!tournament) {\n notFoundError('Tournament not found');\n }\n\n const templates = await notificationTemplateRepository.findTemplates(tournamentId);\n const winner = templates.get(TournamentNotificationTag.MATCH_RESULT_WINNER);\n const config = await matchEvaluationOrchestrator.resolveCouponConfig(tournamentId);\n\t\tif (!config) return;\n\n for (const m of payload.matches) {\n const matchId = new mongoose.Types.ObjectId(m.matchId.toString());\n const { winnerIds } = await predictionService.evaluate(tournamentId, matchId, m.finalScore);\n\n logger.info('Predictions evaluated', {\n tag: 'tournament.prediction.evaluate',\n data: { tournamentId: tournamentId.toString(), matchId: matchId.toString(), matchNumber: m.matchNumber, finalScore: m.finalScore, winnerCount: winnerIds.length, winnerIds },\n });\n\n if (config && winnerIds.length > 0) {\n const couponDetails = await rewardService.grant(\n RewardType.COUPON,\n RewardBuilder.buildCouponRewardContext(matchId, tournamentId, winnerIds, m.matchNumber, config)\n );\n\n logger.info('Coupon reward granted', {\n tag: 'tournament.prediction.evaluate',\n data: { tournamentId: tournamentId.toString(), matchId: matchId.toString(), winnerCount: winnerIds.length, couponDetails },\n });\n\n await tournamentNotificationService.notifyUsers(\n {\n title: winner?.title ?? '🎉 Your prediction was currect.',\n body: winner?.body ?? 'Your coupon is now available',\n tournamentId: tournamentId.toString(),\n tournamentNotificationType: TournamentNotificationTag.MATCH_RESULT_WINNER,\n deepLinkRoute: tournament.deepLinkRoute,\n matchId: matchId.toString(),\n couponCode: (couponDetails as CouponRewardResult).code,\n couponOfferLabel: (couponDetails as CouponRewardResult).couponOfferLabel,\n couponValidityLabel: (couponDetails as CouponRewardResult).couponValidityLabel,\n couponStartDate: (couponDetails as CouponRewardResult).duration.start,\n couponEndDate: (couponDetails as CouponRewardResult).duration.end,\n },\n winnerIds\n );\n }\n }\n });\n}\n"]}
1
+ {"version":3,"file":"evaluate-predictions.consumer.js","sourceRoot":"/","sources":["modules/tournament/match/events/evaluate-predictions.consumer.ts"],"names":[],"mappings":"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","sourcesContent":["import {mongoose} from '@typegoose/typegoose';\nimport {EvaluatePredictionsPayload, TOURNAMENT_EVALUATE_PREDICTIONS_EVENT} from './evaluate-predictions.event';\nimport {predictionService} from '../../prediction/prediction.service';\nimport {rewardService} from '../../reward/reward.service';\nimport {tournamentNotificationService} from '../../notification/notification.service';\nimport {notificationTemplateRepository} from '../../notification/notification-template.repository';\nimport {TournamentNotificationTag} from '@lyxa.ai/core/dist/utilities/enum';\nimport {RewardBuilder} from '../../reward/reward.builder';\nimport {CouponRewardResult, RewardType} from '../../reward/reward.types';\nimport {subscribeToEvent} from '@lyxa.ai/core/dist/libraries/event';\nimport {tournamentConfigRepository} from '@modules/tournament/config/config.repository';\nimport {notFoundError} from '@lyxa.ai/core/dist/utilities/error-common';\nimport {matchEvaluationOrchestrator} from \"@modules/tournament/match/events/match-evaluation.orchestrator\";\nimport {getLibraries} from '@lyxa.ai/core';\n\nexport async function EvaluatePredictionsConsumer() {\n await subscribeToEvent(TOURNAMENT_EVALUATE_PREDICTIONS_EVENT, async (payload: EvaluatePredictionsPayload) => {\n if (!payload.matches?.length) return;\n const tournamentId = new mongoose.Types.ObjectId(payload.tournamentId.toString());\n const logger = getLibraries().getLoggerKit();\n\n logger.info('Evaluate predictions event received', {\n tag: 'tournament.prediction.evaluate',\n data: { tournamentId: tournamentId.toString(), matchCount: payload.matches.length, matches: payload.matches },\n });\n\n const tournament = await tournamentConfigRepository.findById(tournamentId);\n if (!tournament) {\n notFoundError('Tournament not found');\n }\n\n const templates = await notificationTemplateRepository.findTemplates(tournamentId);\n const winner = templates.get(TournamentNotificationTag.MATCH_RESULT_WINNER);\n const config = await matchEvaluationOrchestrator.resolveCouponConfig(tournamentId);\n\t\tif (!config) return;\n\n for (const m of payload.matches) {\n const matchId = new mongoose.Types.ObjectId(m.matchId.toString());\n const { winnerIds } = await predictionService.evaluate(tournamentId, matchId, m.finalScore);\n\n logger.info('Predictions evaluated', {\n tag: 'tournament.prediction.evaluate',\n data: { tournamentId: tournamentId.toString(), matchId: matchId.toString(), matchNumber: m.matchNumber, finalScore: m.finalScore, winnerCount: winnerIds.length, winnerIds },\n });\n\n if (config && winnerIds.length > 0) {\n const couponDetails = await rewardService.grant(\n RewardType.COUPON,\n RewardBuilder.buildCouponRewardContext(matchId, tournamentId, winnerIds, m.matchNumber, config)\n );\n\n logger.info('Coupon reward granted', {\n tag: 'tournament.prediction.evaluate',\n data: { tournamentId: tournamentId.toString(), matchId: matchId.toString(), winnerCount: winnerIds.length, couponDetails },\n });\n\n await tournamentNotificationService.notifyUsers(\n {\n title: winner?.title ?? '🎉 Your prediction was correct.',\n body: winner?.body ?? 'Your coupon is now available',\n tournamentId: tournamentId.toString(),\n tournamentNotificationType: TournamentNotificationTag.MATCH_RESULT_WINNER,\n deepLinkRoute: tournament.deepLinkRoute,\n matchId: matchId.toString(),\n couponCode: (couponDetails as CouponRewardResult).code,\n couponOfferLabel: (couponDetails as CouponRewardResult).couponOfferLabel,\n couponValidityLabel: (couponDetails as CouponRewardResult).couponValidityLabel,\n couponStartDate: (couponDetails as CouponRewardResult).duration.start,\n couponEndDate: (couponDetails as CouponRewardResult).duration.end,\n },\n winnerIds\n );\n }\n }\n });\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=notification.service.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification.service.spec.d.ts","sourceRoot":"/","sources":["modules/tournament/notification/__tests__/notification.service.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,266 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ jest.mock('typedi', () => ({
7
+ __esModule: true,
8
+ default: { get: jest.fn() },
9
+ Container: { get: jest.fn() },
10
+ Service: () => (target) => target,
11
+ }));
12
+ jest.mock('../../participant/participant.service', () => ({
13
+ ParticipantService: class {
14
+ },
15
+ participantService: {},
16
+ }));
17
+ const publishEventMock = jest.fn().mockResolvedValue(undefined);
18
+ jest.mock('@lyxa.ai/core/dist/libraries/event', () => ({
19
+ publishEvent: (...args) => publishEventMock(...args),
20
+ }));
21
+ const loggerMock = { info: jest.fn(), error: jest.fn() };
22
+ jest.mock('@lyxa.ai/core', () => ({
23
+ getLibraries: () => ({ getLoggerKit: () => loggerMock }),
24
+ }));
25
+ const mongoose_1 = require("mongoose");
26
+ const typedi_1 = __importDefault(require("typedi"));
27
+ const enum_1 = require("@lyxa.ai/core/dist/utilities/enum");
28
+ const notification_service_1 = require("../notification.service");
29
+ describe('TournamentNotificationService', () => {
30
+ const participantService = {
31
+ getPredicted: jest.fn(),
32
+ getUnpredicted: jest.fn(),
33
+ };
34
+ let service;
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+ typedi_1.default.get.mockReturnValue(participantService);
38
+ service = new notification_service_1.TournamentNotificationService();
39
+ });
40
+ function makeMatch(overrides = {}) {
41
+ return {
42
+ _id: new mongoose_1.Types.ObjectId(),
43
+ stage: 'group',
44
+ team1: { teamId: new mongoose_1.Types.ObjectId(), name: 'Lions' },
45
+ team2: { teamId: new mongoose_1.Types.ObjectId(), name: 'Tigers' },
46
+ score: { team1: null, team2: null },
47
+ winnerId: null,
48
+ startingDate: new Date('2026-06-20T18:00:00Z'),
49
+ matchEnded: false,
50
+ notificationFlags: new Map(),
51
+ ...overrides,
52
+ };
53
+ }
54
+ function makeTournament(overrides = {}) {
55
+ return {
56
+ _id: new mongoose_1.Types.ObjectId(),
57
+ deepLinkRoute: '/tournament/123',
58
+ ...overrides,
59
+ };
60
+ }
61
+ function makeParticipant(overrides = {}) {
62
+ return {
63
+ _id: new mongoose_1.Types.ObjectId(),
64
+ userId: new mongoose_1.Types.ObjectId(),
65
+ tournamentId: new mongoose_1.Types.ObjectId(),
66
+ status: 'joined',
67
+ predictions: [],
68
+ ...overrides,
69
+ };
70
+ }
71
+ function makeTemplates(tag, title, body) {
72
+ return new Map([[tag, { title, body }]]);
73
+ }
74
+ describe('notifyUsers', () => {
75
+ it('publishes one notification per recipient and logs success', async () => {
76
+ const userIds = [new mongoose_1.Types.ObjectId(), new mongoose_1.Types.ObjectId()];
77
+ await service.notifyUsers({
78
+ title: 'Prediction reminder',
79
+ body: 'You have not predicted yet',
80
+ tournamentNotificationType: enum_1.TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,
81
+ tournamentId: 'tournament-1',
82
+ }, userIds);
83
+ expect(publishEventMock).toHaveBeenCalledTimes(userIds.length);
84
+ const recipientIds = publishEventMock.mock.calls.map(([event]) => event.payload.data.recipientId);
85
+ expect(recipientIds).toEqual(userIds);
86
+ expect(loggerMock.info).toHaveBeenCalledWith('Tournament user notifications dispatched', expect.objectContaining({ data: expect.objectContaining({ recipientCount: userIds.length }) }));
87
+ expect(loggerMock.error).not.toHaveBeenCalled();
88
+ });
89
+ it('does not publish anything for an empty recipient list but still logs', async () => {
90
+ await service.notifyUsers({
91
+ title: 'Prediction reminder',
92
+ body: 'You have not predicted yet',
93
+ tournamentNotificationType: enum_1.TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,
94
+ tournamentId: 'tournament-1',
95
+ }, []);
96
+ expect(publishEventMock).not.toHaveBeenCalled();
97
+ expect(loggerMock.info).toHaveBeenCalledWith('Tournament user notifications dispatched', expect.objectContaining({ data: expect.objectContaining({ recipientCount: 0 }) }));
98
+ });
99
+ it('catches publish failures and logs the error instead of throwing', async () => {
100
+ publishEventMock.mockRejectedValueOnce(new Error('broker unreachable'));
101
+ await expect(service.notifyUsers({
102
+ title: 'Prediction reminder',
103
+ body: 'You have not predicted yet',
104
+ tournamentNotificationType: enum_1.TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,
105
+ tournamentId: 'tournament-1',
106
+ }, [new mongoose_1.Types.ObjectId()])).resolves.toBeUndefined();
107
+ expect(loggerMock.error).toHaveBeenCalledWith('Failed to dispatch tournament user notifications', expect.any(Error), expect.anything());
108
+ expect(loggerMock.info).not.toHaveBeenCalled();
109
+ });
110
+ });
111
+ describe('dispatchScheduledReminder', () => {
112
+ it('sends a single global notification for GLOBAL target without resolving recipients', async () => {
113
+ const match = makeMatch();
114
+ const tournament = makeTournament();
115
+ const ctx = {
116
+ type: enum_1.TournamentNotificationTag.TEAM_SELECTION_REMINDER_1_DAY,
117
+ target: enum_1.TournamentScheduledNotificationTarget.GLOBAL,
118
+ templates: makeTemplates(enum_1.TournamentNotificationTag.TEAM_SELECTION_REMINDER_1_DAY, '{team1} vs {team2} kicks off soon', 'Pick your team for {team1} vs {team2}'),
119
+ tournament,
120
+ match,
121
+ };
122
+ await service.dispatchScheduledReminder(ctx);
123
+ expect(participantService.getPredicted).not.toHaveBeenCalled();
124
+ expect(participantService.getUnpredicted).not.toHaveBeenCalled();
125
+ expect(publishEventMock).toHaveBeenCalledTimes(1);
126
+ const event = publishEventMock.mock.calls[0][0];
127
+ expect(event.payload.title).toBe('Lions vs Tigers kicks off soon');
128
+ expect(event.payload.body).toBe('Pick your team for Lions vs Tigers');
129
+ expect(event.payload.data.isGlobal).toBe(true);
130
+ expect(event.payload.data.matchId).toBeUndefined();
131
+ expect(event.payload.data.predictionExpiryTime).toBeUndefined();
132
+ });
133
+ it('resolves predicted participants and notifies them for PREDICTION_REMINDER_1_HOUR', async () => {
134
+ const match = makeMatch();
135
+ const tournament = makeTournament();
136
+ const predicted = [makeParticipant(), makeParticipant()];
137
+ participantService.getPredicted.mockResolvedValue(predicted);
138
+ const ctx = {
139
+ type: enum_1.TournamentNotificationTag.PREDICTION_REMINDER_1_HOUR,
140
+ target: enum_1.TournamentScheduledNotificationTarget.PREDICTED_PARTICIPANTS,
141
+ templates: makeTemplates(enum_1.TournamentNotificationTag.PREDICTION_REMINDER_1_HOUR, '{team1} vs {team2} starts in 1 hour', 'Your prediction for {team1} vs {team2} locks soon'),
142
+ tournament,
143
+ match,
144
+ };
145
+ await service.dispatchScheduledReminder(ctx);
146
+ expect(participantService.getPredicted).toHaveBeenCalledWith(tournament._id, match._id);
147
+ expect(participantService.getUnpredicted).not.toHaveBeenCalled();
148
+ expect(publishEventMock).toHaveBeenCalledTimes(predicted.length);
149
+ const event = publishEventMock.mock.calls[0][0];
150
+ expect(event.payload.title).toBe('Lions vs Tigers starts in 1 hour');
151
+ expect(event.payload.data.matchId).toBe(match._id.toString());
152
+ expect(event.payload.data.predictionExpiryTime).toBe(match.startingDate);
153
+ });
154
+ it('resolves unpredicted participants and notifies them for PREDICTION_REMINDER_3_HOURS', async () => {
155
+ const match = makeMatch();
156
+ const tournament = makeTournament();
157
+ const unpredicted = [makeParticipant(), makeParticipant()];
158
+ participantService.getUnpredicted.mockResolvedValue(unpredicted);
159
+ const ctx = {
160
+ type: enum_1.TournamentNotificationTag.PREDICTION_REMINDER_3_HOURS,
161
+ target: enum_1.TournamentScheduledNotificationTarget.PREDICTION_PENDING_PARTICIPANTS,
162
+ templates: makeTemplates(enum_1.TournamentNotificationTag.PREDICTION_REMINDER_3_HOURS, '{team1} vs {team2} starts in 3 hours', 'Predict {team1} vs {team2} now'),
163
+ tournament,
164
+ match,
165
+ };
166
+ await service.dispatchScheduledReminder(ctx);
167
+ expect(participantService.getUnpredicted).toHaveBeenCalledWith(tournament._id, [match.team1.teamId, match.team2.teamId], match._id);
168
+ expect(participantService.getPredicted).not.toHaveBeenCalled();
169
+ expect(publishEventMock).toHaveBeenCalledTimes(unpredicted.length);
170
+ const event = publishEventMock.mock.calls[0][0];
171
+ expect(event.payload.title).toBe('Lions vs Tigers starts in 3 hours');
172
+ expect(event.payload.data.matchId).toBe(match._id.toString());
173
+ expect(event.payload.data.predictionExpiryTime).toBe(match.startingDate);
174
+ });
175
+ it('resolves unpredicted participants and notifies them for PREDICTION_REMINDER_15_MINUTES', async () => {
176
+ const match = makeMatch();
177
+ const tournament = makeTournament();
178
+ const unpredicted = [makeParticipant()];
179
+ participantService.getUnpredicted.mockResolvedValue(unpredicted);
180
+ const ctx = {
181
+ type: enum_1.TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,
182
+ target: enum_1.TournamentScheduledNotificationTarget.PREDICTION_PENDING_PARTICIPANTS,
183
+ templates: makeTemplates(enum_1.TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES, '{team1} vs {team2} starts in 15 minutes', 'Last call to predict {team1} vs {team2}'),
184
+ tournament,
185
+ match,
186
+ };
187
+ await service.dispatchScheduledReminder(ctx);
188
+ expect(participantService.getUnpredicted).toHaveBeenCalledWith(tournament._id, [match.team1.teamId, match.team2.teamId], match._id);
189
+ expect(participantService.getPredicted).not.toHaveBeenCalled();
190
+ expect(publishEventMock).toHaveBeenCalledTimes(unpredicted.length);
191
+ const event = publishEventMock.mock.calls[0][0];
192
+ expect(event.payload.title).toBe('Lions vs Tigers starts in 15 minutes');
193
+ expect(event.payload.data.recipientId).toEqual(unpredicted[0].userId);
194
+ });
195
+ it('short-circuits without notifying when there are no recipients', async () => {
196
+ const match = makeMatch();
197
+ const tournament = makeTournament();
198
+ participantService.getUnpredicted.mockResolvedValue([]);
199
+ const ctx = {
200
+ type: enum_1.TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,
201
+ target: enum_1.TournamentScheduledNotificationTarget.PREDICTION_PENDING_PARTICIPANTS,
202
+ templates: makeTemplates(enum_1.TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES, 'Last call for {team1} vs {team2}', 'Predict now'),
203
+ tournament,
204
+ match,
205
+ };
206
+ await service.dispatchScheduledReminder(ctx);
207
+ expect(publishEventMock).not.toHaveBeenCalled();
208
+ expect(loggerMock.info).not.toHaveBeenCalled();
209
+ });
210
+ it('throws when no template exists for the requested tag', async () => {
211
+ const match = makeMatch();
212
+ const tournament = makeTournament();
213
+ const ctx = {
214
+ type: enum_1.TournamentNotificationTag.MATCH_RESULT_WINNER,
215
+ target: enum_1.TournamentScheduledNotificationTarget.GLOBAL,
216
+ templates: new Map(),
217
+ tournament,
218
+ match,
219
+ };
220
+ await expect(service.dispatchScheduledReminder(ctx)).rejects.toThrow(`No notification template for tag: ${enum_1.TournamentNotificationTag.MATCH_RESULT_WINNER}`);
221
+ });
222
+ });
223
+ describe('match result notifications', () => {
224
+ it('notifies winners with their coupon reward details attached', async () => {
225
+ const match = makeMatch();
226
+ const winnerIds = [new mongoose_1.Types.ObjectId(), new mongoose_1.Types.ObjectId()];
227
+ await service.notifyUsers({
228
+ title: 'Your prediction was correct',
229
+ body: 'Your coupon is now available',
230
+ tournamentNotificationType: enum_1.TournamentNotificationTag.MATCH_RESULT_WINNER,
231
+ tournamentId: 'tournament-1',
232
+ deepLinkRoute: '/deep/link',
233
+ matchId: match._id.toString(),
234
+ couponCode: 'WIN-50',
235
+ couponOfferLabel: '50% off',
236
+ }, winnerIds);
237
+ expect(publishEventMock).toHaveBeenCalledTimes(winnerIds.length);
238
+ const payloads = publishEventMock.mock.calls.map(([event]) => event.payload);
239
+ expect(payloads.map(p => p.data.recipientId)).toEqual(winnerIds);
240
+ payloads.forEach(payload => {
241
+ expect(payload.data).toMatchObject({
242
+ tournamentNotificationType: enum_1.TournamentNotificationTag.MATCH_RESULT_WINNER,
243
+ matchId: match._id.toString(),
244
+ couponCode: 'WIN-50',
245
+ couponOfferLabel: '50% off',
246
+ });
247
+ });
248
+ });
249
+ it('notifies losers without coupon reward details', async () => {
250
+ const match = makeMatch();
251
+ const loserIds = [new mongoose_1.Types.ObjectId()];
252
+ await service.notifyUsers({
253
+ title: 'Better luck next time',
254
+ body: 'Your prediction did not match the result',
255
+ tournamentNotificationType: enum_1.TournamentNotificationTag.MATCH_RESULT_LOSER,
256
+ tournamentId: 'tournament-1',
257
+ matchId: match._id.toString(),
258
+ }, loserIds);
259
+ expect(publishEventMock).toHaveBeenCalledTimes(loserIds.length);
260
+ const event = publishEventMock.mock.calls[0][0];
261
+ expect(event.payload.data.tournamentNotificationType).toBe(enum_1.TournamentNotificationTag.MATCH_RESULT_LOSER);
262
+ expect(event.payload.data.couponCode).toBeUndefined();
263
+ });
264
+ });
265
+ });
266
+ //# sourceMappingURL=notification.service.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification.service.spec.js","sourceRoot":"/","sources":["modules/tournament/notification/__tests__/notification.service.spec.ts"],"names":[],"mappings":";;;;;AAiBA,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC;IACvB,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,EAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,EAAC;IACzB,SAAS,EAAE,EAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,EAAC;IAC3B,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,MAAW,EAAE,EAAE,CAAC,MAAM;CACzC,CAAC,CAAC,CAAC;AAEJ,IAAI,CAAC,IAAI,CAAC,uCAAuC,EAAE,GAAG,EAAE,CAAC,CAAC;IACtD,kBAAkB,EAAE;KAAQ;IAC5B,kBAAkB,EAAE,EAAE;CACzB,CAAC,CAAC,CAAC;AAEJ,MAAM,gBAAgB,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;AAChE,IAAI,CAAC,IAAI,CAAC,oCAAoC,EAAE,GAAG,EAAE,CAAC,CAAC;IACnD,YAAY,EAAE,CAAC,GAAG,IAAW,EAAE,EAAE,CAAC,gBAAgB,CAAC,GAAG,IAAI,CAAC;CAC9D,CAAC,CAAC,CAAC;AAEJ,MAAM,UAAU,GAAG,EAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,EAAC,CAAC;AACvD,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,CAAC,CAAC;IAC9B,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAC,YAAY,EAAE,GAAG,EAAE,CAAC,UAAU,EAAC,CAAC;CACzD,CAAC,CAAC,CAAC;AAEJ,uCAA+B;AAC/B,oDAA+B;AAC/B,4DAAmH;AAEnH,kEAAgG;AAIhG,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC3C,MAAM,kBAAkB,GAAG;QACvB,YAAY,EAAE,IAAI,CAAC,EAAE,EAAE;QACvB,cAAc,EAAE,IAAI,CAAC,EAAE,EAAE;KAC5B,CAAC;IAEF,IAAI,OAAsC,CAAC;IAE3C,UAAU,CAAC,GAAG,EAAE;QACZ,IAAI,CAAC,aAAa,EAAE,CAAC;QACpB,gBAAS,CAAC,GAAiB,CAAC,eAAe,CAAC,kBAAkB,CAAC,CAAC;QACjE,OAAO,GAAG,IAAI,oDAA6B,EAAE,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,SAAS,SAAS,CAAC,YAAsC,EAAE;QACvD,OAAO;YACH,GAAG,EAAE,IAAI,gBAAK,CAAC,QAAQ,EAAE;YACzB,KAAK,EAAE,OAAO;YACd,KAAK,EAAE,EAAC,MAAM,EAAE,IAAI,gBAAK,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,OAAO,EAAC;YACpD,KAAK,EAAE,EAAC,MAAM,EAAE,IAAI,gBAAK,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAC;YACrD,KAAK,EAAE,EAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAC;YACjC,QAAQ,EAAE,IAAI;YACd,YAAY,EAAE,IAAI,IAAI,CAAC,sBAAsB,CAAC;YAC9C,UAAU,EAAE,KAAK;YACjB,iBAAiB,EAAE,IAAI,GAAG,EAAE;YAC5B,GAAG,SAAS;SACI,CAAC;IACzB,CAAC;IAED,SAAS,cAAc,CAAC,YAAyC,EAAE;QAC/D,OAAO;YACH,GAAG,EAAE,IAAI,gBAAK,CAAC,QAAQ,EAAE;YACzB,aAAa,EAAE,iBAAiB;YAChC,GAAG,SAAS;SACO,CAAC;IAC5B,CAAC;IAED,SAAS,eAAe,CAAC,YAAqD,EAAE;QAC5E,OAAO;YACH,GAAG,EAAE,IAAI,gBAAK,CAAC,QAAQ,EAAE;YACzB,MAAM,EAAE,IAAI,gBAAK,CAAC,QAAQ,EAAE;YAC5B,YAAY,EAAE,IAAI,gBAAK,CAAC,QAAQ,EAAE;YAClC,MAAM,EAAE,QAAQ;YAChB,WAAW,EAAE,EAAE;YACf,GAAG,SAAS;SACmB,CAAC;IACxC,CAAC;IAED,SAAS,aAAa,CAAC,GAA8B,EAAE,KAAa,EAAE,IAAY;QAC9E,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,EAAC,KAAK,EAAE,IAAI,EAAwB,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;QACzB,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;YACvE,MAAM,OAAO,GAAG,CAAC,IAAI,gBAAK,CAAC,QAAQ,EAAE,EAAE,IAAI,gBAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;YAE7D,MAAM,OAAO,CAAC,WAAW,CACrB;gBACI,KAAK,EAAE,qBAAqB;gBAC5B,IAAI,EAAE,4BAA4B;gBAClC,0BAA0B,EAAE,gCAAyB,CAAC,8BAA8B;gBACpF,YAAY,EAAE,cAAc;aAC/B,EACD,OAAO,CACV,CAAC;YAEF,MAAM,CAAC,gBAAgB,CAAC,CAAC,qBAAqB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAC/D,MAAM,YAAY,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YAClG,MAAM,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,oBAAoB,CACxC,0CAA0C,EAC1C,MAAM,CAAC,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,CAAC,gBAAgB,CAAC,EAAC,cAAc,EAAE,OAAO,CAAC,MAAM,EAAC,CAAC,EAAC,CAAC,CAC7F,CAAC;YACF,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QACpD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;YAClF,MAAM,OAAO,CAAC,WAAW,CACrB;gBACI,KAAK,EAAE,qBAAqB;gBAC5B,IAAI,EAAE,4BAA4B;gBAClC,0BAA0B,EAAE,gCAAyB,CAAC,8BAA8B;gBACpF,YAAY,EAAE,cAAc;aAC/B,EACD,EAAE,CACL,CAAC;YAEF,MAAM,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YAChD,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,oBAAoB,CACxC,0CAA0C,EAC1C,MAAM,CAAC,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,CAAC,gBAAgB,CAAC,EAAC,cAAc,EAAE,CAAC,EAAC,CAAC,EAAC,CAAC,CAChF,CAAC;QACN,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;YAC7E,gBAAgB,CAAC,qBAAqB,CAAC,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC,CAAC;YAExE,MAAM,MAAM,CACR,OAAO,CAAC,WAAW,CACf;gBACI,KAAK,EAAE,qBAAqB;gBAC5B,IAAI,EAAE,4BAA4B;gBAClC,0BAA0B,EAAE,gCAAyB,CAAC,8BAA8B;gBACpF,YAAY,EAAE,cAAc;aAC/B,EACD,CAAC,IAAI,gBAAK,CAAC,QAAQ,EAAE,CAAC,CACzB,CACJ,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;YAE3B,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,oBAAoB,CACzC,kDAAkD,EAClD,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EACjB,MAAM,CAAC,QAAQ,EAAE,CACpB,CAAC;YACF,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QACnD,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAEH,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACvC,EAAE,CAAC,mFAAmF,EAAE,KAAK,IAAI,EAAE;YAC/F,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;YAC1B,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;YACpC,MAAM,GAAG,GAA6B;gBAClC,IAAI,EAAE,gCAAyB,CAAC,6BAA6B;gBAC7D,MAAM,EAAE,4CAAqC,CAAC,MAAM;gBACpD,SAAS,EAAE,aAAa,CACpB,gCAAyB,CAAC,6BAA6B,EACvD,mCAAmC,EACnC,uCAAuC,CAC1C;gBACD,UAAU;gBACV,KAAK;aACR,CAAC;YAEF,MAAM,OAAO,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC;YAE7C,MAAM,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YAC/D,MAAM,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YACjE,MAAM,CAAC,gBAAgB,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;YAElD,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;YACnE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;YACtE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/C,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,CAAC;YACnD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,aAAa,EAAE,CAAC;QACpE,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;YAC9F,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;YAC1B,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;YACpC,MAAM,SAAS,GAAG,CAAC,eAAe,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC;YACzD,kBAAkB,CAAC,YAAY,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;YAE7D,MAAM,GAAG,GAA6B;gBAClC,IAAI,EAAE,gCAAyB,CAAC,0BAA0B;gBAC1D,MAAM,EAAE,4CAAqC,CAAC,sBAAsB;gBACpE,SAAS,EAAE,aAAa,CACpB,gCAAyB,CAAC,0BAA0B,EACpD,qCAAqC,EACrC,mDAAmD,CACtD;gBACD,UAAU;gBACV,KAAK;aACR,CAAC;YAEF,MAAM,OAAO,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC;YAE7C,MAAM,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,oBAAoB,CAAC,UAAU,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;YACxF,MAAM,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YACjE,MAAM,CAAC,gBAAgB,CAAC,CAAC,qBAAqB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAEjE,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;YACrE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC9D,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAC7E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,qFAAqF,EAAE,KAAK,IAAI,EAAE;YACjG,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;YAC1B,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;YACpC,MAAM,WAAW,GAAG,CAAC,eAAe,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC;YAC3D,kBAAkB,CAAC,cAAc,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;YAEjE,MAAM,GAAG,GAA6B;gBAClC,IAAI,EAAE,gCAAyB,CAAC,2BAA2B;gBAC3D,MAAM,EAAE,4CAAqC,CAAC,+BAA+B;gBAC7E,SAAS,EAAE,aAAa,CACpB,gCAAyB,CAAC,2BAA2B,EACrD,sCAAsC,EACtC,gCAAgC,CACnC;gBACD,UAAU;gBACV,KAAK;aACR,CAAC;YAEF,MAAM,OAAO,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC;YAE7C,MAAM,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,oBAAoB,CAC1D,UAAU,CAAC,GAAG,EACd,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EACxC,KAAK,CAAC,GAAG,CACZ,CAAC;YACF,MAAM,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YAC/D,MAAM,CAAC,gBAAgB,CAAC,CAAC,qBAAqB,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAEnE,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;YACtE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC9D,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;QAC7E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,wFAAwF,EAAE,KAAK,IAAI,EAAE;YACpG,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;YAC1B,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;YACpC,MAAM,WAAW,GAAG,CAAC,eAAe,EAAE,CAAC,CAAC;YACxC,kBAAkB,CAAC,cAAc,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;YAEjE,MAAM,GAAG,GAA6B;gBAClC,IAAI,EAAE,gCAAyB,CAAC,8BAA8B;gBAC9D,MAAM,EAAE,4CAAqC,CAAC,+BAA+B;gBAC7E,SAAS,EAAE,aAAa,CACpB,gCAAyB,CAAC,8BAA8B,EACxD,yCAAyC,EACzC,yCAAyC,CAC5C;gBACD,UAAU;gBACV,KAAK;aACR,CAAC;YAEF,MAAM,OAAO,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC;YAE7C,MAAM,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC,oBAAoB,CAC1D,UAAU,CAAC,GAAG,EACd,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,EACxC,KAAK,CAAC,GAAG,CACZ,CAAC;YACF,MAAM,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YAC/D,MAAM,CAAC,gBAAgB,CAAC,CAAC,qBAAqB,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAEnE,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;YACzE,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+DAA+D,EAAE,KAAK,IAAI,EAAE;YAC3E,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;YAC1B,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;YACpC,kBAAkB,CAAC,cAAc,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;YAExD,MAAM,GAAG,GAA6B;gBAClC,IAAI,EAAE,gCAAyB,CAAC,8BAA8B;gBAC9D,MAAM,EAAE,4CAAqC,CAAC,+BAA+B;gBAC7E,SAAS,EAAE,aAAa,CACpB,gCAAyB,CAAC,8BAA8B,EACxD,kCAAkC,EAClC,aAAa,CAChB;gBACD,UAAU;gBACV,KAAK;aACR,CAAC;YAEF,MAAM,OAAO,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC;YAE7C,MAAM,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YAChD,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QACnD,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;YAClE,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;YAC1B,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;YAEpC,MAAM,GAAG,GAA6B;gBAClC,IAAI,EAAE,gCAAyB,CAAC,mBAAmB;gBACnD,MAAM,EAAE,4CAAqC,CAAC,MAAM;gBACpD,SAAS,EAAE,IAAI,GAAG,EAAE;gBACpB,UAAU;gBACV,KAAK;aACR,CAAC;YAEF,MAAM,MAAM,CAAC,OAAO,CAAC,yBAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAChE,qCAAqC,gCAAyB,CAAC,mBAAmB,EAAE,CACvF,CAAC;QACN,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IAKH,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACxC,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;YACxE,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;YAC1B,MAAM,SAAS,GAAG,CAAC,IAAI,gBAAK,CAAC,QAAQ,EAAE,EAAE,IAAI,gBAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;YAE/D,MAAM,OAAO,CAAC,WAAW,CACrB;gBACI,KAAK,EAAE,6BAA6B;gBACpC,IAAI,EAAE,8BAA8B;gBACpC,0BAA0B,EAAE,gCAAyB,CAAC,mBAAmB;gBACzE,YAAY,EAAE,cAAc;gBAC5B,aAAa,EAAE,YAAY;gBAC3B,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE;gBAC7B,UAAU,EAAE,QAAQ;gBACpB,gBAAgB,EAAE,SAAS;aAC9B,EACD,SAAS,CACZ,CAAC;YAEF,MAAM,CAAC,gBAAgB,CAAC,CAAC,qBAAqB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YACjE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC7E,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACjE,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE;gBACvB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC;oBAC/B,0BAA0B,EAAE,gCAAyB,CAAC,mBAAmB;oBACzE,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE;oBAC7B,UAAU,EAAE,QAAQ;oBACpB,gBAAgB,EAAE,SAAS;iBAC9B,CAAC,CAAC;YACP,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,CAAC;QAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;YAC3D,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;YAC1B,MAAM,QAAQ,GAAG,CAAC,IAAI,gBAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;YAExC,MAAM,OAAO,CAAC,WAAW,CACrB;gBACI,KAAK,EAAE,uBAAuB;gBAC9B,IAAI,EAAE,0CAA0C;gBAChD,0BAA0B,EAAE,gCAAyB,CAAC,kBAAkB;gBACxE,YAAY,EAAE,cAAc;gBAC5B,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE;aAChC,EACD,QAAQ,CACX,CAAC;YAEF,MAAM,CAAC,gBAAgB,CAAC,CAAC,qBAAqB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAChE,MAAM,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC,IAAI,CAAC,gCAAyB,CAAC,kBAAkB,CAAC,CAAC;YACzG,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,aAAa,EAAE,CAAC;QAC1D,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC","sourcesContent":["/**\n * TournamentNotificationService unit tests\n *\n * Covers:\n * - notifyGlobal: builds and publishes a NotificationGlobalEvent with tournament-specific overrides\n * - notifyUsers: publishes one NotificationEvent per recipient, logs success, and on failure\n * catches the error and logs it instead of throwing\n * - dispatchScheduledReminder:\n * - GLOBAL target dispatches a single global notification without resolving recipients\n * - PREDICTION_REMINDER_1_HOUR resolves recipients via getPredicted (predicted participants)\n * - any other tag resolves recipients via getUnpredicted (unpredicted participants)\n * - empty recipient list short-circuits without calling notifyUsers\n * - targeted messages include matchId + predictionExpiryTime, global/base messages don't\n * - {team1}/{team2} placeholders are substituted in both title and body\n * - missing template for the requested tag throws\n */\n\njest.mock('typedi', () => ({\n __esModule: true,\n default: {get: jest.fn()},\n Container: {get: jest.fn()},\n Service: () => (target: any) => target,\n}));\n\njest.mock('../../participant/participant.service', () => ({\n ParticipantService: class {},\n participantService: {},\n}));\n\nconst publishEventMock = jest.fn().mockResolvedValue(undefined);\njest.mock('@lyxa.ai/core/dist/libraries/event', () => ({\n publishEvent: (...args: any[]) => publishEventMock(...args),\n}));\n\nconst loggerMock = {info: jest.fn(), error: jest.fn()};\njest.mock('@lyxa.ai/core', () => ({\n getLibraries: () => ({getLoggerKit: () => loggerMock}),\n}));\n\nimport {Types} from 'mongoose';\nimport Container from 'typedi';\nimport {TournamentNotificationTag, TournamentScheduledNotificationTarget} from '@lyxa.ai/core/dist/utilities/enum';\nimport {NotificationMessage, TournamentMatch} from '@lyxa.ai/core/dist/libraries/mongo/models';\nimport {TournamentNotificationService, ScheduledReminderContext} from '../notification.service';\nimport {TournamentParticipantOutputDTO} from '../../participant/participant.validation';\nimport {TournamentDocument} from '../../config/config.types';\n\ndescribe('TournamentNotificationService', () => {\n const participantService = {\n getPredicted: jest.fn(),\n getUnpredicted: jest.fn(),\n };\n\n let service: TournamentNotificationService;\n\n beforeEach(() => {\n jest.clearAllMocks();\n (Container.get as jest.Mock).mockReturnValue(participantService);\n service = new TournamentNotificationService();\n });\n\n function makeMatch(overrides: Partial<TournamentMatch> = {}): TournamentMatch {\n return {\n _id: new Types.ObjectId(),\n stage: 'group',\n team1: {teamId: new Types.ObjectId(), name: 'Lions'},\n team2: {teamId: new Types.ObjectId(), name: 'Tigers'},\n score: {team1: null, team2: null},\n winnerId: null,\n startingDate: new Date('2026-06-20T18:00:00Z'),\n matchEnded: false,\n notificationFlags: new Map(),\n ...overrides,\n } as TournamentMatch;\n }\n\n function makeTournament(overrides: Partial<TournamentDocument> = {}): TournamentDocument {\n return {\n _id: new Types.ObjectId(),\n deepLinkRoute: '/tournament/123',\n ...overrides,\n } as TournamentDocument;\n }\n\n function makeParticipant(overrides: Partial<TournamentParticipantOutputDTO> = {}): TournamentParticipantOutputDTO {\n return {\n _id: new Types.ObjectId(),\n userId: new Types.ObjectId(),\n tournamentId: new Types.ObjectId(),\n status: 'joined',\n predictions: [],\n ...overrides,\n } as TournamentParticipantOutputDTO;\n }\n\n function makeTemplates(tag: TournamentNotificationTag, title: string, body: string): Map<TournamentNotificationTag, NotificationMessage> {\n return new Map([[tag, {title, body} as NotificationMessage]]);\n }\n\n describe('notifyUsers', () => {\n it('publishes one notification per recipient and logs success', async () => {\n const userIds = [new Types.ObjectId(), new Types.ObjectId()];\n\n await service.notifyUsers(\n {\n title: 'Prediction reminder',\n body: 'You have not predicted yet',\n tournamentNotificationType: TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,\n tournamentId: 'tournament-1',\n },\n userIds\n );\n\n expect(publishEventMock).toHaveBeenCalledTimes(userIds.length);\n const recipientIds = publishEventMock.mock.calls.map(([event]) => event.payload.data.recipientId);\n expect(recipientIds).toEqual(userIds);\n expect(loggerMock.info).toHaveBeenCalledWith(\n 'Tournament user notifications dispatched',\n expect.objectContaining({data: expect.objectContaining({recipientCount: userIds.length})})\n );\n expect(loggerMock.error).not.toHaveBeenCalled();\n });\n\n it('does not publish anything for an empty recipient list but still logs', async () => {\n await service.notifyUsers(\n {\n title: 'Prediction reminder',\n body: 'You have not predicted yet',\n tournamentNotificationType: TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,\n tournamentId: 'tournament-1',\n },\n []\n );\n\n expect(publishEventMock).not.toHaveBeenCalled();\n expect(loggerMock.info).toHaveBeenCalledWith(\n 'Tournament user notifications dispatched',\n expect.objectContaining({data: expect.objectContaining({recipientCount: 0})})\n );\n });\n\n it('catches publish failures and logs the error instead of throwing', async () => {\n publishEventMock.mockRejectedValueOnce(new Error('broker unreachable'));\n\n await expect(\n service.notifyUsers(\n {\n title: 'Prediction reminder',\n body: 'You have not predicted yet',\n tournamentNotificationType: TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,\n tournamentId: 'tournament-1',\n },\n [new Types.ObjectId()]\n )\n ).resolves.toBeUndefined();\n\n expect(loggerMock.error).toHaveBeenCalledWith(\n 'Failed to dispatch tournament user notifications',\n expect.any(Error),\n expect.anything()\n );\n expect(loggerMock.info).not.toHaveBeenCalled();\n });\n });\n\n describe('dispatchScheduledReminder', () => {\n it('sends a single global notification for GLOBAL target without resolving recipients', async () => {\n const match = makeMatch();\n const tournament = makeTournament();\n const ctx: ScheduledReminderContext = {\n type: TournamentNotificationTag.TEAM_SELECTION_REMINDER_1_DAY,\n target: TournamentScheduledNotificationTarget.GLOBAL,\n templates: makeTemplates(\n TournamentNotificationTag.TEAM_SELECTION_REMINDER_1_DAY,\n '{team1} vs {team2} kicks off soon',\n 'Pick your team for {team1} vs {team2}'\n ),\n tournament,\n match,\n };\n\n await service.dispatchScheduledReminder(ctx);\n\n expect(participantService.getPredicted).not.toHaveBeenCalled();\n expect(participantService.getUnpredicted).not.toHaveBeenCalled();\n expect(publishEventMock).toHaveBeenCalledTimes(1);\n\n const event = publishEventMock.mock.calls[0][0];\n expect(event.payload.title).toBe('Lions vs Tigers kicks off soon');\n expect(event.payload.body).toBe('Pick your team for Lions vs Tigers');\n expect(event.payload.data.isGlobal).toBe(true);\n expect(event.payload.data.matchId).toBeUndefined();\n expect(event.payload.data.predictionExpiryTime).toBeUndefined();\n });\n\n it('resolves predicted participants and notifies them for PREDICTION_REMINDER_1_HOUR', async () => {\n const match = makeMatch();\n const tournament = makeTournament();\n const predicted = [makeParticipant(), makeParticipant()];\n participantService.getPredicted.mockResolvedValue(predicted);\n\n const ctx: ScheduledReminderContext = {\n type: TournamentNotificationTag.PREDICTION_REMINDER_1_HOUR,\n target: TournamentScheduledNotificationTarget.PREDICTED_PARTICIPANTS,\n templates: makeTemplates(\n TournamentNotificationTag.PREDICTION_REMINDER_1_HOUR,\n '{team1} vs {team2} starts in 1 hour',\n 'Your prediction for {team1} vs {team2} locks soon'\n ),\n tournament,\n match,\n };\n\n await service.dispatchScheduledReminder(ctx);\n\n expect(participantService.getPredicted).toHaveBeenCalledWith(tournament._id, match._id);\n expect(participantService.getUnpredicted).not.toHaveBeenCalled();\n expect(publishEventMock).toHaveBeenCalledTimes(predicted.length);\n\n const event = publishEventMock.mock.calls[0][0];\n expect(event.payload.title).toBe('Lions vs Tigers starts in 1 hour');\n expect(event.payload.data.matchId).toBe(match._id.toString());\n expect(event.payload.data.predictionExpiryTime).toBe(match.startingDate);\n });\n\n it('resolves unpredicted participants and notifies them for PREDICTION_REMINDER_3_HOURS', async () => {\n const match = makeMatch();\n const tournament = makeTournament();\n const unpredicted = [makeParticipant(), makeParticipant()];\n participantService.getUnpredicted.mockResolvedValue(unpredicted);\n\n const ctx: ScheduledReminderContext = {\n type: TournamentNotificationTag.PREDICTION_REMINDER_3_HOURS,\n target: TournamentScheduledNotificationTarget.PREDICTION_PENDING_PARTICIPANTS,\n templates: makeTemplates(\n TournamentNotificationTag.PREDICTION_REMINDER_3_HOURS,\n '{team1} vs {team2} starts in 3 hours',\n 'Predict {team1} vs {team2} now'\n ),\n tournament,\n match,\n };\n\n await service.dispatchScheduledReminder(ctx);\n\n expect(participantService.getUnpredicted).toHaveBeenCalledWith(\n tournament._id,\n [match.team1.teamId, match.team2.teamId],\n match._id\n );\n expect(participantService.getPredicted).not.toHaveBeenCalled();\n expect(publishEventMock).toHaveBeenCalledTimes(unpredicted.length);\n\n const event = publishEventMock.mock.calls[0][0];\n expect(event.payload.title).toBe('Lions vs Tigers starts in 3 hours');\n expect(event.payload.data.matchId).toBe(match._id.toString());\n expect(event.payload.data.predictionExpiryTime).toBe(match.startingDate);\n });\n\n it('resolves unpredicted participants and notifies them for PREDICTION_REMINDER_15_MINUTES', async () => {\n const match = makeMatch();\n const tournament = makeTournament();\n const unpredicted = [makeParticipant()];\n participantService.getUnpredicted.mockResolvedValue(unpredicted);\n\n const ctx: ScheduledReminderContext = {\n type: TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,\n target: TournamentScheduledNotificationTarget.PREDICTION_PENDING_PARTICIPANTS,\n templates: makeTemplates(\n TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,\n '{team1} vs {team2} starts in 15 minutes',\n 'Last call to predict {team1} vs {team2}'\n ),\n tournament,\n match,\n };\n\n await service.dispatchScheduledReminder(ctx);\n\n expect(participantService.getUnpredicted).toHaveBeenCalledWith(\n tournament._id,\n [match.team1.teamId, match.team2.teamId],\n match._id\n );\n expect(participantService.getPredicted).not.toHaveBeenCalled();\n expect(publishEventMock).toHaveBeenCalledTimes(unpredicted.length);\n\n const event = publishEventMock.mock.calls[0][0];\n expect(event.payload.title).toBe('Lions vs Tigers starts in 15 minutes');\n expect(event.payload.data.recipientId).toEqual(unpredicted[0].userId);\n });\n\n it('short-circuits without notifying when there are no recipients', async () => {\n const match = makeMatch();\n const tournament = makeTournament();\n participantService.getUnpredicted.mockResolvedValue([]);\n\n const ctx: ScheduledReminderContext = {\n type: TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,\n target: TournamentScheduledNotificationTarget.PREDICTION_PENDING_PARTICIPANTS,\n templates: makeTemplates(\n TournamentNotificationTag.PREDICTION_REMINDER_15_MINUTES,\n 'Last call for {team1} vs {team2}',\n 'Predict now'\n ),\n tournament,\n match,\n };\n\n await service.dispatchScheduledReminder(ctx);\n\n expect(publishEventMock).not.toHaveBeenCalled();\n expect(loggerMock.info).not.toHaveBeenCalled();\n });\n\n it('throws when no template exists for the requested tag', async () => {\n const match = makeMatch();\n const tournament = makeTournament();\n\n const ctx: ScheduledReminderContext = {\n type: TournamentNotificationTag.MATCH_RESULT_WINNER,\n target: TournamentScheduledNotificationTarget.GLOBAL,\n templates: new Map(),\n tournament,\n match,\n };\n\n await expect(service.dispatchScheduledReminder(ctx)).rejects.toThrow(\n `No notification template for tag: ${TournamentNotificationTag.MATCH_RESULT_WINNER}`\n );\n });\n });\n\n // Match-result notifications (winner/loser) aren't dispatched through dispatchScheduledReminder -\n // evaluate-predictions.consumer.ts calls notifyUsers directly once a match is evaluated, attaching\n // reward-specific extra fields via the NotificationObject index signature.\n describe('match result notifications', () => {\n it('notifies winners with their coupon reward details attached', async () => {\n const match = makeMatch();\n const winnerIds = [new Types.ObjectId(), new Types.ObjectId()];\n\n await service.notifyUsers(\n {\n title: 'Your prediction was correct',\n body: 'Your coupon is now available',\n tournamentNotificationType: TournamentNotificationTag.MATCH_RESULT_WINNER,\n tournamentId: 'tournament-1',\n deepLinkRoute: '/deep/link',\n matchId: match._id.toString(),\n couponCode: 'WIN-50',\n couponOfferLabel: '50% off',\n },\n winnerIds\n );\n\n expect(publishEventMock).toHaveBeenCalledTimes(winnerIds.length);\n const payloads = publishEventMock.mock.calls.map(([event]) => event.payload);\n expect(payloads.map(p => p.data.recipientId)).toEqual(winnerIds);\n payloads.forEach(payload => {\n expect(payload.data).toMatchObject({\n tournamentNotificationType: TournamentNotificationTag.MATCH_RESULT_WINNER,\n matchId: match._id.toString(),\n couponCode: 'WIN-50',\n couponOfferLabel: '50% off',\n });\n });\n });\n\n it('notifies losers without coupon reward details', async () => {\n const match = makeMatch();\n const loserIds = [new Types.ObjectId()];\n\n await service.notifyUsers(\n {\n title: 'Better luck next time',\n body: 'Your prediction did not match the result',\n tournamentNotificationType: TournamentNotificationTag.MATCH_RESULT_LOSER,\n tournamentId: 'tournament-1',\n matchId: match._id.toString(),\n },\n loserIds\n );\n\n expect(publishEventMock).toHaveBeenCalledTimes(loserIds.length);\n const event = publishEventMock.mock.calls[0][0];\n expect(event.payload.data.tournamentNotificationType).toBe(TournamentNotificationTag.MATCH_RESULT_LOSER);\n expect(event.payload.data.couponCode).toBeUndefined();\n });\n });\n});\n"]}
@@ -1,15 +1,26 @@
1
1
  import { mongoose } from '@typegoose/typegoose';
2
2
  import { TournamentNotificationTag, TournamentScheduledNotificationTarget } from '@lyxa.ai/core/dist/utilities/enum';
3
- import { NotificationMessage } from '@lyxa.ai/core/dist/libraries/mongo/models';
3
+ import { NotificationMessage, TournamentMatch } from '@lyxa.ai/core/dist/libraries/mongo/models';
4
+ import { TournamentDocument } from '../config/config.types';
4
5
  import { NotificationObject } from './notification.config';
6
+ export interface ScheduledReminderContext {
7
+ type: TournamentNotificationTag;
8
+ target: TournamentScheduledNotificationTarget;
9
+ templates: Map<TournamentNotificationTag, NotificationMessage>;
10
+ tournament: TournamentDocument;
11
+ match: TournamentMatch;
12
+ }
5
13
  export declare class TournamentNotificationService {
6
14
  private participantService;
7
15
  constructor();
8
16
  private getTemplate;
9
- private hasNotPredicted;
10
17
  notifyGlobal(notification: NotificationObject): Promise<void>;
11
18
  notifyUsers(notification: NotificationObject, userIds: mongoose.Types.ObjectId[]): Promise<void>;
12
- dispatchScheduledJob(type: TournamentNotificationTag, target: TournamentScheduledNotificationTarget, templates: Map<TournamentNotificationTag, NotificationMessage>, tournamentId: mongoose.Types.ObjectId, teamIds: mongoose.Types.ObjectId[], matchId: mongoose.Types.ObjectId, matchStartTime: Date, deepLinkRoute?: string): Promise<void>;
19
+ dispatchScheduledReminder(ctx: ScheduledReminderContext): Promise<void>;
20
+ private resolveReminderRecipients;
21
+ private buildBaseMessage;
22
+ private buildTargetedMessage;
23
+ private withTeamNames;
13
24
  }
14
25
  export declare const tournamentNotificationService: TournamentNotificationService;
15
26
  //# sourceMappingURL=notification.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notification.service.d.ts","sourceRoot":"/","sources":["modules/tournament/notification/notification.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AAG9C,OAAO,EAGH,yBAAyB,EACzB,qCAAqC,EACxC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAC,mBAAmB,EAAC,MAAM,2CAA2C,CAAC;AAG9E,OAAO,EAAC,kBAAkB,EAAC,MAAM,uBAAuB,CAAC;AAEzD,qBACa,6BAA6B;IACtC,OAAO,CAAC,kBAAkB,CAAqB;;IAM/C,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,eAAe;IAIV,YAAY,CAAC,YAAY,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB7D,WAAW,CAAC,YAAY,EAAE,kBAAkB,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBhG,oBAAoB,CAC7B,IAAI,EAAE,yBAAyB,EAC/B,MAAM,EAAE,qCAAqC,EAC7C,SAAS,EAAE,GAAG,CAAC,yBAAyB,EAAE,mBAAmB,CAAC,EAC9D,YAAY,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,EACrC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,EAClC,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAChC,cAAc,EAAE,IAAI,EACpB,aAAa,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC;CAqBnB;AAED,eAAO,MAAM,6BAA6B,+BAA+C,CAAC"}
1
+ {"version":3,"file":"notification.service.d.ts","sourceRoot":"/","sources":["modules/tournament/notification/notification.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,QAAQ,EAAC,MAAM,sBAAsB,CAAC;AAI9C,OAAO,EAGH,yBAAyB,EACzB,qCAAqC,EACxC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAC,mBAAmB,EAAE,eAAe,EAAC,MAAM,2CAA2C,CAAC;AAG/F,OAAO,EAAC,kBAAkB,EAAC,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAAC,kBAAkB,EAAC,MAAM,uBAAuB,CAAC;AAEzD,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,yBAAyB,CAAC;IAChC,MAAM,EAAE,qCAAqC,CAAC;IAC9C,SAAS,EAAE,GAAG,CAAC,yBAAyB,EAAE,mBAAmB,CAAC,CAAC;IAC/D,UAAU,EAAE,kBAAkB,CAAC;IAC/B,KAAK,EAAE,eAAe,CAAC;CAC1B;AAED,qBACa,6BAA6B;IACtC,OAAO,CAAC,kBAAkB,CAAqB;;IAM/C,OAAO,CAAC,WAAW;IAMN,YAAY,CAAC,YAAY,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiB7D,WAAW,CAAC,YAAY,EAAE,kBAAkB,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA6DhG,yBAAyB,CAAC,GAAG,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAYpF,OAAO,CAAC,yBAAyB;IAcjC,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,oBAAoB;IAQ5B,OAAO,CAAC,aAAa;CAKxB;AAED,eAAO,MAAM,6BAA6B,+BAA+C,CAAC"}