@jsm-mit/sultana-core-motoko-package 0.3.0 → 0.4.0

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.
@@ -75,6 +75,16 @@ type UpdateSalonServiceArgs =
75
75
  serviceTypeIds: vec ServiceTypeId;
76
76
  workerIds: vec principal;
77
77
  };
78
+ type UpdatePromoArgs =
79
+ record {
80
+ assetId: AssetId;
81
+ chips: vec text;
82
+ cta: PromoCta;
83
+ promoId: PromoId;
84
+ salonId: SalonId;
85
+ target: PromoTarget;
86
+ textLines: vec text;
87
+ };
78
88
  type UpdateProfileArgs =
79
89
  record {
80
90
  avatarUrl: opt text;
@@ -127,6 +137,12 @@ type SetReportThresholdArgs =
127
137
  opType: OperationType;
128
138
  thresholdMs: nat;
129
139
  };
140
+ type SetPromoActiveArgs =
141
+ record {
142
+ active: bool;
143
+ promoId: PromoId;
144
+ salonId: SalonId;
145
+ };
130
146
  type SetLanguageActiveArgs =
131
147
  record {
132
148
  active: bool;
@@ -497,6 +513,7 @@ type PromoTarget =
497
513
  };
498
514
  type PromoItem =
499
515
  record {
516
+ active: bool;
500
517
  assetId: AssetId;
501
518
  chips: vec text;
502
519
  createdAt: int;
@@ -856,8 +873,9 @@ service : {
856
873
  addDiscountCode: (args: AddDiscountCodeArgs) -> (Result_1);
857
874
  /// Superadmin: adds a supported language (active by default; languages are never deleted)
858
875
  addLanguage: (code: LangCode) -> (Result_1);
859
- /// Owner attaches a promo (max 3 per salon): #Ready media from the salon's library,
860
- /// target = the salon or one of its services, CTA = button label / show-delay / discount code
876
+ /// Owner adds a promo to the salon's library (max 15): #Ready media from the salon's
877
+ /// library, target = the salon or one of its services, CTA = button label / discount code.
878
+ /// The promo is created INACTIVE — setPromoActive is what puts it in the feed
861
879
  addPromo: (args: AddPromoArgs) -> (Result_37);
862
880
  /// Adds a new service to a specific salon
863
881
  addSalonService: (args: AddSalonServiceArgs) -> (Result_36);
@@ -942,7 +960,7 @@ service : {
942
960
  /// The salon's outstanding invite, or null when there is none or it has expired (Owner only)
943
961
  getSalonInvite: (salonId: SalonId) -> (Result_20) query;
944
962
  getSalonInviteTtl: () -> (Result_3) query;
945
- /// Public: the salon's current promos
963
+ /// The salon's promos: the whole library for its owner, the active ones for everyone else
946
964
  getSalonPromos: (salonId: SalonId) -> (Result_19) query;
947
965
  /// Returns the list of salon services by salon ID (owner view)
948
966
  getSalonServicesBySalonId: (salonId: SalonId) -> (Result_6) query;
@@ -983,7 +1001,7 @@ service : {
983
1001
  removeDailySchedule: (args: RemoveDailyScheduleArgs) -> (Result_1);
984
1002
  /// Owner removes a code (blocked while a promo of the salon still broadcasts it)
985
1003
  removeDiscountCode: (args: RemoveDiscountCodeArgs) -> (Result_1);
986
- /// Owner removes a promo (this is also the "deactivate"); the library asset stays
1004
+ /// Owner deletes a promo for good (to only hide it, use setPromoActive); the asset stays
987
1005
  removePromo: (args: RemovePromoArgs) -> (Result_1);
988
1006
  /// Permanently removes a salon service and cleans up search indexes
989
1007
  removeSalonServiceBySalonServiceId: (salonServiceId: SalonServiceId) ->
@@ -1015,6 +1033,9 @@ service : {
1015
1033
  /// Superadmin only, ONE-WAY: flips isProd to true — the test-only endpoints
1016
1034
  /// (purgeFailedAssetsNow, triggerAssetsSweep) trap from then on. There is no way back.
1017
1035
  setProdMode: () -> (Result_1);
1036
+ /// Owner puts a promo in the feed or takes it out (max 3 active per salon); the promo
1037
+ /// itself stays in the library either way
1038
+ setPromoActive: (args: SetPromoActiveArgs) -> (Result_1);
1018
1039
  setReportBatchSize: (size: nat) -> (Result_3);
1019
1040
  setReportThreshold: (args: SetReportThresholdArgs) -> (Result_3);
1020
1041
  setSalonInviteTtl: (newTtlSeconds: nat) -> (Result_3);
@@ -1031,6 +1052,8 @@ service : {
1031
1052
  /// sweep now, with a TTL override — same code path its hourly timer drives
1032
1053
  triggerAssetsSweep: (ttlSeconds: nat) -> (Result_3);
1033
1054
  updateMyProfile: (args: UpdateProfileArgs) -> (Result_2);
1055
+ /// Owner edits a promo in place; it keeps its id, its createdAt and its place in the feed
1056
+ updatePromo: (args: UpdatePromoArgs) -> (Result_1);
1034
1057
  /// Updates an existing service with new details and synchronizes the index
1035
1058
  updateSalonService: (args: UpdateSalonServiceArgs) -> (Result_1);
1036
1059
  updateVisit: (args: UpdateVisitArgs) -> (Result);
@@ -265,6 +265,7 @@ export interface PromoItem {
265
265
  'cta' : PromoCta,
266
266
  'textLines' : Array<string>,
267
267
  'thumbId' : AssetId,
268
+ 'active' : boolean,
268
269
  'assetId' : AssetId,
269
270
  'chips' : Array<string>,
270
271
  'kind' : AssetKind,
@@ -479,6 +480,11 @@ export interface SetDailyScheduleArgs {
479
480
  'mask' : TimeMask,
480
481
  }
481
482
  export interface SetLanguageActiveArgs { 'active' : boolean, 'code' : LangCode }
483
+ export interface SetPromoActiveArgs {
484
+ 'active' : boolean,
485
+ 'salonId' : SalonId,
486
+ 'promoId' : PromoId,
487
+ }
482
488
  export interface SetReportThresholdArgs {
483
489
  'opType' : OperationType,
484
490
  'thresholdMs' : bigint,
@@ -516,6 +522,15 @@ export interface UpdateProfileArgs {
516
522
  'name' : string,
517
523
  'avatarUrl' : [] | [string],
518
524
  }
525
+ export interface UpdatePromoArgs {
526
+ 'cta' : PromoCta,
527
+ 'textLines' : Array<string>,
528
+ 'assetId' : AssetId,
529
+ 'chips' : Array<string>,
530
+ 'target' : PromoTarget,
531
+ 'salonId' : SalonId,
532
+ 'promoId' : PromoId,
533
+ }
519
534
  export interface UpdateSalonServiceArgs {
520
535
  'duration' : bigint,
521
536
  'active' : boolean,
@@ -596,8 +611,9 @@ export interface _SERVICE {
596
611
  */
597
612
  'addLanguage' : ActorMethod<[LangCode], Result_1>,
598
613
  /**
599
- * / Owner attaches a promo (max 3 per salon): #Ready media from the salon's library,
600
- * / target = the salon or one of its services, CTA = button label / show-delay / discount code
614
+ * / Owner adds a promo to the salon's library (max 15): #Ready media from the salon's
615
+ * / library, target = the salon or one of its services, CTA = button label / discount code.
616
+ * / The promo is created INACTIVE — setPromoActive is what puts it in the feed
601
617
  */
602
618
  'addPromo' : ActorMethod<[AddPromoArgs], Result_37>,
603
619
  /**
@@ -759,7 +775,7 @@ export interface _SERVICE {
759
775
  'getSalonInvite' : ActorMethod<[SalonId], Result_20>,
760
776
  'getSalonInviteTtl' : ActorMethod<[], Result_3>,
761
777
  /**
762
- * / Public: the salon's current promos
778
+ * / The salon's promos: the whole library for its owner, the active ones for everyone else
763
779
  */
764
780
  'getSalonPromos' : ActorMethod<[SalonId], Result_19>,
765
781
  /**
@@ -828,7 +844,7 @@ export interface _SERVICE {
828
844
  */
829
845
  'removeDiscountCode' : ActorMethod<[RemoveDiscountCodeArgs], Result_1>,
830
846
  /**
831
- * / Owner removes a promo (this is also the "deactivate"); the library asset stays
847
+ * / Owner deletes a promo for good (to only hide it, use setPromoActive); the asset stays
832
848
  */
833
849
  'removePromo' : ActorMethod<[RemovePromoArgs], Result_1>,
834
850
  /**
@@ -888,6 +904,11 @@ export interface _SERVICE {
888
904
  * / (purgeFailedAssetsNow, triggerAssetsSweep) trap from then on. There is no way back.
889
905
  */
890
906
  'setProdMode' : ActorMethod<[], Result_1>,
907
+ /**
908
+ * / Owner puts a promo in the feed or takes it out (max 3 active per salon); the promo
909
+ * / itself stays in the library either way
910
+ */
911
+ 'setPromoActive' : ActorMethod<[SetPromoActiveArgs], Result_1>,
891
912
  'setReportBatchSize' : ActorMethod<[bigint], Result_3>,
892
913
  'setReportThreshold' : ActorMethod<[SetReportThresholdArgs], Result_3>,
893
914
  'setSalonInviteTtl' : ActorMethod<[bigint], Result_3>,
@@ -910,6 +931,10 @@ export interface _SERVICE {
910
931
  */
911
932
  'triggerAssetsSweep' : ActorMethod<[bigint], Result_3>,
912
933
  'updateMyProfile' : ActorMethod<[UpdateProfileArgs], Result_2>,
934
+ /**
935
+ * / Owner edits a promo in place; it keeps its id, its createdAt and its place in the feed
936
+ */
937
+ 'updatePromo' : ActorMethod<[UpdatePromoArgs], Result_1>,
913
938
  /**
914
939
  * / Updates an existing service with new details and synchronizes the index
915
940
  */
@@ -397,6 +397,7 @@ export const idlFactory = ({ IDL }) => {
397
397
  'cta' : PromoCta,
398
398
  'textLines' : IDL.Vec(IDL.Text),
399
399
  'thumbId' : AssetId,
400
+ 'active' : IDL.Bool,
400
401
  'assetId' : AssetId,
401
402
  'chips' : IDL.Vec(IDL.Text),
402
403
  'kind' : AssetKind,
@@ -591,6 +592,11 @@ export const idlFactory = ({ IDL }) => {
591
592
  'active' : IDL.Bool,
592
593
  'code' : LangCode,
593
594
  });
595
+ const SetPromoActiveArgs = IDL.Record({
596
+ 'active' : IDL.Bool,
597
+ 'salonId' : SalonId,
598
+ 'promoId' : PromoId,
599
+ });
594
600
  const SetReportThresholdArgs = IDL.Record({
595
601
  'opType' : OperationType,
596
602
  'thresholdMs' : IDL.Nat,
@@ -606,6 +612,15 @@ export const idlFactory = ({ IDL }) => {
606
612
  'name' : IDL.Text,
607
613
  'avatarUrl' : IDL.Opt(IDL.Text),
608
614
  });
615
+ const UpdatePromoArgs = IDL.Record({
616
+ 'cta' : PromoCta,
617
+ 'textLines' : IDL.Vec(IDL.Text),
618
+ 'assetId' : AssetId,
619
+ 'chips' : IDL.Vec(IDL.Text),
620
+ 'target' : PromoTarget,
621
+ 'salonId' : SalonId,
622
+ 'promoId' : PromoId,
623
+ });
609
624
  const UpdateSalonServiceArgs = IDL.Record({
610
625
  'duration' : IDL.Nat,
611
626
  'active' : IDL.Bool,
@@ -809,6 +824,7 @@ export const idlFactory = ({ IDL }) => {
809
824
  'setLanguageActive' : IDL.Func([SetLanguageActiveArgs], [Result_1], []),
810
825
  'setLoggingEnabled' : IDL.Func([IDL.Bool], [Result_1], []),
811
826
  'setProdMode' : IDL.Func([], [Result_1], []),
827
+ 'setPromoActive' : IDL.Func([SetPromoActiveArgs], [Result_1], []),
812
828
  'setReportBatchSize' : IDL.Func([IDL.Nat], [Result_3], []),
813
829
  'setReportThreshold' : IDL.Func([SetReportThresholdArgs], [Result_3], []),
814
830
  'setSalonInviteTtl' : IDL.Func([IDL.Nat], [Result_3], []),
@@ -820,6 +836,7 @@ export const idlFactory = ({ IDL }) => {
820
836
  'setWeeklyTemplate' : IDL.Func([SetWeeklyTemplateArgs], [Result_1], []),
821
837
  'triggerAssetsSweep' : IDL.Func([IDL.Nat], [Result_3], []),
822
838
  'updateMyProfile' : IDL.Func([UpdateProfileArgs], [Result_2], []),
839
+ 'updatePromo' : IDL.Func([UpdatePromoArgs], [Result_1], []),
823
840
  'updateSalonService' : IDL.Func([UpdateSalonServiceArgs], [Result_1], []),
824
841
  'updateVisit' : IDL.Func([UpdateVisitArgs], [Result], []),
825
842
  });
@@ -1,6 +1,10 @@
1
1
  import { type Identity } from "@icp-sdk/core/agent";
2
2
  import { ActorBase } from "../actor-base.js";
3
3
  import type { PromoItem } from "../../declarations/sultana-core-motoko/sultana-core-motoko.did.js";
4
+ /** How many promos a salon may KEEP — the size of its library, drafts included (PromoRepo). */
5
+ export declare const MAX_PROMOS_PER_SALON = 15;
6
+ /** How many of them may be IN THE FEED at once (PromoRepo). */
7
+ export declare const MAX_ACTIVE_PROMOS_PER_SALON = 3;
4
8
  /** Friendly promo-content input — every field optional; omitted = empty / frontend default. */
5
9
  export interface PromoContentInput {
6
10
  /** Up to 2 overlay text lines rendered over the media. */
@@ -13,46 +17,82 @@ export interface PromoContentInput {
13
17
  discountCode?: string;
14
18
  }
15
19
  /**
16
- * Wraps the promo endpoints on sultana-core-motoko. Promos belong to a SALON (max 3 per
17
- * salon) and are managed by its current owner; the media must be a #Ready asset from the
18
- * salon's library. getSalonPromos and getPromoFeed are public queries — the feed follows
19
- * the search pattern: the frontend expands its radius into geohash6 cells and adapts the
20
- * set to the response size, core only answers exact-key lookups.
20
+ * Wraps the promo endpoints on sultana-core-motoko. A salon keeps a LIBRARY of up to
21
+ * MAX_PROMOS_PER_SALON promos, of which at most MAX_ACTIVE_PROMOS_PER_SALON may be active —
22
+ * only the active ones reach the geo feed. Promos are managed by the salon's current owner
23
+ * and the media must be a #Ready asset from the salon's library.
24
+ *
25
+ * getSalonPromos and getPromoFeed are public queries. The feed follows the search pattern: the
26
+ * caller expands its radius into geohash6 cells and adapts the set to the response size, core
27
+ * only answers exact-key lookups.
21
28
  */
22
29
  export declare class PromoActor extends ActorBase {
23
30
  constructor(canisterId: string, identity?: Identity);
24
31
  /**
25
- * Attaches a promo to the salon (owner only, max 3 per salon). The media must be a
26
- * #Ready asset from the salon's library (image or video — kind and thumb/poster are
27
- * taken from the library entry).
32
+ * Adds a promo to the salon's library (owner only, max MAX_PROMOS_PER_SALON). The media
33
+ * must be a #Ready asset from the salon's library (image or video — kind and thumb/poster
34
+ * are taken from the library entry).
35
+ *
36
+ * The promo is created INACTIVE: adding never pushes a running campaign out of the feed,
37
+ * so call setPromoActiveAsyncUnsafe to put it there.
28
38
  * @param salonId - The salon the promo belongs to (and, for a salon-target promo, links to).
29
- * @param assetId - A #Ready asset from the salon's library; one asset can back only one promo.
39
+ * @param assetId - A #Ready asset from the salon's library; one asset may back several promos.
30
40
  * @param content - Overlay text lines (max 2), chips (max 2), button label and discount
31
41
  * code (all optional — see PromoContentInput).
32
42
  * @param targetSalonServiceId - Optional: make the promo tap through to this service of the
33
43
  * salon; omitted -> the promo targets the salon itself.
34
44
  * @returns A Promise that resolves to the new PromoId.
35
45
  * @throws Error `CanisterError` on a Framework.Result #err (not the owner, asset not in the
36
- * library or not #Ready, promo limit reached, duplicate asset, bad target, too many
37
- * lines/chips, ...).
46
+ * library or not #Ready, library full, bad target, too many lines/chips, a discount
47
+ * code that is not this salon's or whose scope does not fit the target, ...).
38
48
  * @throws Error `CriticalCanisterError` / `CallRefusedAtInspectionStage` as usual.
39
49
  */
40
50
  addPromoAsyncUnsafe(salonId: string, assetId: string, content?: PromoContentInput, targetSalonServiceId?: string): Promise<string>;
41
51
  /**
42
- * Removes a promo from the salon (owner only). This is also the "deactivate" — there is
43
- * no separate active flag; the library asset stays untouched.
52
+ * Edits a promo in place (owner only). The promo keeps its id, its createdAt and its place
53
+ * in or out of the feed, so correcting a running campaign does not restart it. Every
54
+ * content field is replaced by what you pass — an omitted `content` field clears it.
55
+ * @param salonId - The salon that owns the promo.
56
+ * @param promoId - The promo to edit.
57
+ * @param assetId - A #Ready asset from the salon's library; may differ from the current one
58
+ * (kind and thumb/poster are re-read from the new entry).
59
+ * @param content - The new overlay text lines, chips, button label and discount code.
60
+ * @param targetSalonServiceId - Optional new tap-through target; omitted -> the salon itself.
61
+ * @throws Error `CanisterError` on a Framework.Result #err (not the owner, promo not found,
62
+ * promo of a different salon, plus every validation addPromo does).
63
+ */
64
+ updatePromoAsyncUnsafe(salonId: string, promoId: string, assetId: string, content?: PromoContentInput, targetSalonServiceId?: string): Promise<boolean>;
65
+ /**
66
+ * Puts a promo in the geo feed (`active = true`) or takes it out (owner only). The promo
67
+ * stays in the library either way — this is the "deactivate"; removePromo is a real delete.
68
+ *
69
+ * Setting the flag to what it already is succeeds and changes nothing. Activating one promo
70
+ * too many FAILS rather than dropping a running one: which campaign comes down is the
71
+ * owner's decision, so deactivate one first.
72
+ * @throws Error `CanisterError` on a Framework.Result #err (not the owner, promo not found,
73
+ * promo of a different salon, MAX_ACTIVE_PROMOS_PER_SALON already active).
74
+ */
75
+ setPromoActiveAsyncUnsafe(salonId: string, promoId: string, active: boolean): Promise<boolean>;
76
+ /**
77
+ * Deletes a promo from the salon's library (owner only) — content, id and all. To only take
78
+ * it off the feed use setPromoActiveAsyncUnsafe. The library asset stays untouched.
44
79
  * @throws Error `CanisterError` on a Framework.Result #err (not the owner, promo not
45
80
  * found, promo of a different salon).
46
81
  */
47
82
  removePromoAsyncUnsafe(salonId: string, promoId: string): Promise<boolean>;
48
83
  /**
49
- * Returns the salon's current promos — public query (promo content is public by nature).
84
+ * The salon's promos — a public query, but the answer depends on who asks: the salon's
85
+ * owner gets the whole library, everyone else gets the ACTIVE promos only. Drafts stay
86
+ * private because one may already carry a discount code the owner has not broadcast yet.
87
+ *
88
+ * So call this with the owner's identity to manage the library, and with any identity to
89
+ * read what the public sees.
50
90
  * @throws Error `CriticalCanisterError` on transport/canister failure.
51
91
  */
52
92
  getSalonPromosAsyncUnsafe(salonId: string): Promise<PromoItem[]>;
53
93
  /**
54
- * Public geo feed: promos in the given geohash6 cells, deduplicated by promo id. The
55
- * caller expands its radius into cells and should adapt the set to the response size
94
+ * Public geo feed: the ACTIVE promos in the given geohash6 cells, deduplicated by promo id.
95
+ * The caller expands its radius into cells and should adapt the set to the response size
56
96
  * (there is no server-side pagination; core soft-caps very large responses).
57
97
  * @param geohashes6 - The geohash6 cells covering the viewer's radius.
58
98
  * @throws Error `CriticalCanisterError` on transport/canister failure.
@@ -1 +1 @@
1
- {"version":3,"file":"promo-actor.d.ts","sourceRoot":"","sources":["../../src/actors/promo-actor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAY,SAAS,EAAe,MAAM,mEAAmE,CAAC;AAE1H,+FAA+F;AAC/F,MAAM,WAAW,iBAAiB;IAC9B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAaD;;;;;;GAMG;AACH,qBAAa,UAAW,SAAQ,SAAS;gBAEzB,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ;IAInD;;;;;;;;;;;;;;;OAeG;IACU,mBAAmB,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,iBAAsB,EAC/B,oBAAoB,CAAC,EAAE,MAAM,GAC9B,OAAO,CAAC,MAAM,CAAC;IAalB;;;;;OAKG;IACU,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMvF;;;OAGG;IACU,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAM7E;;;;;;OAMG;IACU,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;CAKnF"}
1
+ {"version":3,"file":"promo-actor.d.ts","sourceRoot":"","sources":["../../src/actors/promo-actor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAY,SAAS,EAAe,MAAM,mEAAmE,CAAC;AAE1H,+FAA+F;AAC/F,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,+DAA+D;AAC/D,eAAO,MAAM,2BAA2B,IAAI,CAAC;AAE7C,+FAA+F;AAC/F,MAAM,WAAW,iBAAiB;IAC9B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAaD;;;;;;;;;GASG;AACH,qBAAa,UAAW,SAAQ,SAAS;gBAEzB,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ;IAInD;;;;;;;;;;;;;;;;;;OAkBG;IACU,mBAAmB,CAC5B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,iBAAsB,EAC/B,oBAAoB,CAAC,EAAE,MAAM,GAC9B,OAAO,CAAC,MAAM,CAAC;IAalB;;;;;;;;;;;;OAYG;IACU,sBAAsB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,iBAAsB,EAC/B,oBAAoB,CAAC,EAAE,MAAM,GAC9B,OAAO,CAAC,OAAO,CAAC;IAcnB;;;;;;;;;OASG;IACU,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAM3G;;;;;OAKG;IACU,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAMvF;;;;;;;;OAQG;IACU,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAM7E;;;;;;OAMG;IACU,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;CAKnF"}
@@ -1,5 +1,9 @@
1
1
  import {} from "@icp-sdk/core/agent";
2
2
  import { ActorBase } from "../actor-base.js";
3
+ /** How many promos a salon may KEEP — the size of its library, drafts included (PromoRepo). */
4
+ export const MAX_PROMOS_PER_SALON = 15;
5
+ /** How many of them may be IN THE FEED at once (PromoRepo). */
6
+ export const MAX_ACTIVE_PROMOS_PER_SALON = 3;
3
7
  function toCandidCta(content) {
4
8
  return {
5
9
  buttonLabel: content.buttonLabel === undefined ? [] : [content.buttonLabel],
@@ -10,30 +14,36 @@ function toCandidTarget(targetSalonServiceId) {
10
14
  return targetSalonServiceId ? { SalonService: targetSalonServiceId } : { Salon: null };
11
15
  }
12
16
  /**
13
- * Wraps the promo endpoints on sultana-core-motoko. Promos belong to a SALON (max 3 per
14
- * salon) and are managed by its current owner; the media must be a #Ready asset from the
15
- * salon's library. getSalonPromos and getPromoFeed are public queries — the feed follows
16
- * the search pattern: the frontend expands its radius into geohash6 cells and adapts the
17
- * set to the response size, core only answers exact-key lookups.
17
+ * Wraps the promo endpoints on sultana-core-motoko. A salon keeps a LIBRARY of up to
18
+ * MAX_PROMOS_PER_SALON promos, of which at most MAX_ACTIVE_PROMOS_PER_SALON may be active —
19
+ * only the active ones reach the geo feed. Promos are managed by the salon's current owner
20
+ * and the media must be a #Ready asset from the salon's library.
21
+ *
22
+ * getSalonPromos and getPromoFeed are public queries. The feed follows the search pattern: the
23
+ * caller expands its radius into geohash6 cells and adapts the set to the response size, core
24
+ * only answers exact-key lookups.
18
25
  */
19
26
  export class PromoActor extends ActorBase {
20
27
  constructor(canisterId, identity) {
21
28
  super(canisterId, identity);
22
29
  }
23
30
  /**
24
- * Attaches a promo to the salon (owner only, max 3 per salon). The media must be a
25
- * #Ready asset from the salon's library (image or video — kind and thumb/poster are
26
- * taken from the library entry).
31
+ * Adds a promo to the salon's library (owner only, max MAX_PROMOS_PER_SALON). The media
32
+ * must be a #Ready asset from the salon's library (image or video — kind and thumb/poster
33
+ * are taken from the library entry).
34
+ *
35
+ * The promo is created INACTIVE: adding never pushes a running campaign out of the feed,
36
+ * so call setPromoActiveAsyncUnsafe to put it there.
27
37
  * @param salonId - The salon the promo belongs to (and, for a salon-target promo, links to).
28
- * @param assetId - A #Ready asset from the salon's library; one asset can back only one promo.
38
+ * @param assetId - A #Ready asset from the salon's library; one asset may back several promos.
29
39
  * @param content - Overlay text lines (max 2), chips (max 2), button label and discount
30
40
  * code (all optional — see PromoContentInput).
31
41
  * @param targetSalonServiceId - Optional: make the promo tap through to this service of the
32
42
  * salon; omitted -> the promo targets the salon itself.
33
43
  * @returns A Promise that resolves to the new PromoId.
34
44
  * @throws Error `CanisterError` on a Framework.Result #err (not the owner, asset not in the
35
- * library or not #Ready, promo limit reached, duplicate asset, bad target, too many
36
- * lines/chips, ...).
45
+ * library or not #Ready, library full, bad target, too many lines/chips, a discount
46
+ * code that is not this salon's or whose scope does not fit the target, ...).
37
47
  * @throws Error `CriticalCanisterError` / `CallRefusedAtInspectionStage` as usual.
38
48
  */
39
49
  async addPromoAsyncUnsafe(salonId, assetId, content = {}, targetSalonServiceId) {
@@ -47,8 +57,45 @@ export class PromoActor extends ActorBase {
47
57
  }));
48
58
  }
49
59
  /**
50
- * Removes a promo from the salon (owner only). This is also the "deactivate" — there is
51
- * no separate active flag; the library asset stays untouched.
60
+ * Edits a promo in place (owner only). The promo keeps its id, its createdAt and its place
61
+ * in or out of the feed, so correcting a running campaign does not restart it. Every
62
+ * content field is replaced by what you pass — an omitted `content` field clears it.
63
+ * @param salonId - The salon that owns the promo.
64
+ * @param promoId - The promo to edit.
65
+ * @param assetId - A #Ready asset from the salon's library; may differ from the current one
66
+ * (kind and thumb/poster are re-read from the new entry).
67
+ * @param content - The new overlay text lines, chips, button label and discount code.
68
+ * @param targetSalonServiceId - Optional new tap-through target; omitted -> the salon itself.
69
+ * @throws Error `CanisterError` on a Framework.Result #err (not the owner, promo not found,
70
+ * promo of a different salon, plus every validation addPromo does).
71
+ */
72
+ async updatePromoAsyncUnsafe(salonId, promoId, assetId, content = {}, targetSalonServiceId) {
73
+ return this.executeFunctionAsyncUnsafe(() => this.actor.updatePromo({
74
+ salonId,
75
+ promoId,
76
+ assetId,
77
+ target: toCandidTarget(targetSalonServiceId),
78
+ textLines: content.textLines ?? [],
79
+ chips: content.chips ?? [],
80
+ cta: toCandidCta(content),
81
+ }));
82
+ }
83
+ /**
84
+ * Puts a promo in the geo feed (`active = true`) or takes it out (owner only). The promo
85
+ * stays in the library either way — this is the "deactivate"; removePromo is a real delete.
86
+ *
87
+ * Setting the flag to what it already is succeeds and changes nothing. Activating one promo
88
+ * too many FAILS rather than dropping a running one: which campaign comes down is the
89
+ * owner's decision, so deactivate one first.
90
+ * @throws Error `CanisterError` on a Framework.Result #err (not the owner, promo not found,
91
+ * promo of a different salon, MAX_ACTIVE_PROMOS_PER_SALON already active).
92
+ */
93
+ async setPromoActiveAsyncUnsafe(salonId, promoId, active) {
94
+ return this.executeFunctionAsyncUnsafe(() => this.actor.setPromoActive({ salonId, promoId, active }));
95
+ }
96
+ /**
97
+ * Deletes a promo from the salon's library (owner only) — content, id and all. To only take
98
+ * it off the feed use setPromoActiveAsyncUnsafe. The library asset stays untouched.
52
99
  * @throws Error `CanisterError` on a Framework.Result #err (not the owner, promo not
53
100
  * found, promo of a different salon).
54
101
  */
@@ -56,15 +103,20 @@ export class PromoActor extends ActorBase {
56
103
  return this.executeFunctionAsyncUnsafe(() => this.actor.removePromo({ salonId, promoId }));
57
104
  }
58
105
  /**
59
- * Returns the salon's current promos — public query (promo content is public by nature).
106
+ * The salon's promos — a public query, but the answer depends on who asks: the salon's
107
+ * owner gets the whole library, everyone else gets the ACTIVE promos only. Drafts stay
108
+ * private because one may already carry a discount code the owner has not broadcast yet.
109
+ *
110
+ * So call this with the owner's identity to manage the library, and with any identity to
111
+ * read what the public sees.
60
112
  * @throws Error `CriticalCanisterError` on transport/canister failure.
61
113
  */
62
114
  async getSalonPromosAsyncUnsafe(salonId) {
63
115
  return this.executeFunctionAsyncUnsafe(() => this.actor.getSalonPromos(salonId));
64
116
  }
65
117
  /**
66
- * Public geo feed: promos in the given geohash6 cells, deduplicated by promo id. The
67
- * caller expands its radius into cells and should adapt the set to the response size
118
+ * Public geo feed: the ACTIVE promos in the given geohash6 cells, deduplicated by promo id.
119
+ * The caller expands its radius into cells and should adapt the set to the response size
68
120
  * (there is no server-side pagination; core soft-caps very large responses).
69
121
  * @param geohashes6 - The geohash6 cells covering the viewer's radius.
70
122
  * @throws Error `CriticalCanisterError` on transport/canister failure.
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export { PromoActor } from "./actors/promo-actor.js";
16
16
  export { DiscountCodesActor } from "./actors/discount-codes-actor.js";
17
17
  export type { DiscountValueInput } from "./actors/discount-codes-actor.js";
18
18
  export type { PromoContentInput } from "./actors/promo-actor.js";
19
+ export { MAX_PROMOS_PER_SALON, MAX_ACTIVE_PROMOS_PER_SALON } from "./actors/promo-actor.js";
19
20
  export type { PromoItem, PromoCta, PromoTarget, PromoId, DiscountCode, DiscountScope, DiscountValue, SupportedLanguage, ServiceTypeLabel, ServiceTypeTranslations, SetTranslationArgs, SalonPublicInfo } from "../declarations/sultana-core-motoko/sultana-core-motoko.did.js";
20
21
  export { SalonServicesActor } from "./actors/salon-services-actor.js";
21
22
  export { SalonsActor } from "./actors/salons-actor.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export { HrActor } from "./actors/hr-actor.js";
8
8
  export { MigrationActor } from "./actors/migration-actor.js";
9
9
  export { PromoActor } from "./actors/promo-actor.js";
10
10
  export { DiscountCodesActor } from "./actors/discount-codes-actor.js";
11
+ export { MAX_PROMOS_PER_SALON, MAX_ACTIVE_PROMOS_PER_SALON } from "./actors/promo-actor.js";
11
12
  export { SalonServicesActor } from "./actors/salon-services-actor.js";
12
13
  export { SalonsActor } from "./actors/salons-actor.js";
13
14
  export { SearchActor } from "./actors/search-actor.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsm-mit/sultana-core-motoko-package",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A TypeScript library for interacting with the Sultana Core Motoko actor on the Internet Computer (IC) platform.",
5
5
  "homepage": "https://github.com/JSM-Sultana/sultana-core-motoko-package#readme",
6
6
  "bugs": {