ad2app-lib 1.50.0 → 1.59.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.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Spec 148 wave 3 (T068/T069 US8, T067 US6, T041 US9): the wire shapes the
3
+ * delegation, the offer-deal publication and the calendar read. Validated
4
+ * through class-validator, the way the backend's global ValidationPipe reads
5
+ * them (an empty construct, then the body's properties assigned).
6
+ */
7
+ import { test } from "node:test";
8
+ import assert from "node:assert/strict";
9
+
10
+ import { ACTING_FOR_HEADER, UpdateGrantScopesDto } from "./agency";
11
+ import { DEALROOM_CALENDAR_KINDS, DealroomCalendarQueryDto } from "./dealroom";
12
+
13
+ const errorsOf = async (dto: object): Promise<string[]> => {
14
+ const { validate } = await import("class-validator");
15
+ return (await validate(dto)).map((e) => e.property).sort();
16
+ };
17
+
18
+ /** What the ValidationPipe does: `new Dto()`, then the body's own keys. */
19
+ const piped = (body: Record<string, unknown>) => Object.assign(new UpdateGrantScopesDto(), body);
20
+
21
+ test("the data-access screen's reads-only save is unchanged and never names deals", async () => {
22
+ assert.deepEqual(await errorsOf(piped({ scopes: ["analytics"] })), []);
23
+ assert.equal(piped({ scopes: ["analytics"] }).deals, undefined);
24
+ assert.deepEqual(await errorsOf(piped({ scopes: [] })), ["scopes"]);
25
+ assert.deepEqual(await errorsOf(piped({})), ["scopes"]);
26
+ });
27
+
28
+ test("`deals` is never a read scope: it cannot ride in `scopes`", async () => {
29
+ assert.deepEqual(await errorsOf(piped({ scopes: ["analytics", "deals"] })), ["scopes"]);
30
+ });
31
+
32
+ test("delegation is its own boolean, with or without the reads", async () => {
33
+ assert.deepEqual(await errorsOf(piped({ deals: true })), []);
34
+ assert.deepEqual(await errorsOf(piped({ deals: false })), []);
35
+ assert.deepEqual(await errorsOf(piped({ scopes: ["posts"], deals: true })), []);
36
+ assert.deepEqual(await errorsOf(piped({ deals: "yes" })), ["deals"]);
37
+ assert.deepEqual(await errorsOf(piped({ scopes: [], deals: true })), ["scopes"]);
38
+ });
39
+
40
+ test("the delegate names the creator in one header, lower-case as Node reads it", () => {
41
+ assert.equal(ACTING_FOR_HEADER, "x-acting-for");
42
+ });
43
+
44
+ test("the calendar window needs both ends as dates", async () => {
45
+ assert.deepEqual(await errorsOf(new DealroomCalendarQueryDto({ from: "2026-10-01", to: "2026-11-01" })), []);
46
+ assert.deepEqual(await errorsOf(new DealroomCalendarQueryDto({ from: "soon" })), ["from", "to"]);
47
+ });
48
+
49
+ test("the calendar's marker kinds, in order", () => {
50
+ assert.deepEqual([...DEALROOM_CALENDAR_KINDS], [
51
+ "milestone",
52
+ "application_deadline",
53
+ "materials_due",
54
+ "publication_due",
55
+ "publish_at",
56
+ "bonus_ends",
57
+ "keep_up_ends",
58
+ ]);
59
+ });
60
+
61
+ test("wave 3b: the delegate-consent code, the roster's act-for flag and the followers gap's platform", async () => {
62
+ const { DEALROOM_CODES } = await import("./dealroom");
63
+ assert.equal(DEALROOM_CODES.DELEGATE_CONSENT_FORBIDDEN, "dealroom-delegate-consent-forbidden");
64
+ const gap: import("./dealroom").I_NearMatchGap = { kind: "followers", platform: "tiktok", followers: 8_800, band: { min: 10_000 }, percent: 12, direction: "under" };
65
+ assert.equal(gap.kind === "followers" && gap.platform, "tiktok");
66
+ const row: import("./agency").I_AgencyRosterRow = { grantId: "g", creator: { id: "c", handle: "@a" }, platforms: [], state: "active", grantedScopes: ["analytics"], canActForCreator: true };
67
+ assert.equal(row.canActForCreator, true);
68
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Spec 148 wave 4 (US5 T029/T030, US6 T032/T033, US7 T036/T037), DORMANT:
3
+ * the wire shapes of the contract step, the deal money and the payouts.
4
+ * Validated through class-validator, the way the backend's global
5
+ * ValidationPipe reads them (an empty construct, then the body's keys).
6
+ */
7
+ import { test } from "node:test";
8
+ import assert from "node:assert/strict";
9
+
10
+ import { AGENCY_CAMPAIGN_EVENT_KINDS } from "./agency-campaigns";
11
+ import {
12
+ CreateDealContractDto,
13
+ DEAL_CONTRACT_SOURCES,
14
+ DEAL_CONTRACT_STATES,
15
+ DEAL_MONEY_ACTIONS,
16
+ DEAL_PAYMENT_METHODS,
17
+ DEAL_SIGNATURE_LEVELS,
18
+ DEALROOM_CODES,
19
+ DEALROOM_MONEY_EVENT_KINDS,
20
+ PAYOUT_PROFILE_FIELDS,
21
+ StartDealPaymentDto,
22
+ UpsertPayoutProfileDto,
23
+ } from "./dealroom";
24
+
25
+ const errorsOf = async (dto: object): Promise<string[]> => {
26
+ const { validate } = await import("class-validator");
27
+ return (await validate(dto)).map((e) => e.property).sort();
28
+ };
29
+
30
+ test("the contract step's vocabulary (data-model §dealroom_contracts)", () => {
31
+ assert.deepEqual([...DEAL_CONTRACT_SOURCES], ["template", "uploaded"]);
32
+ assert.deepEqual([...DEAL_CONTRACT_STATES], ["draft", "sent", "signed"]);
33
+ assert.deepEqual([...DEAL_SIGNATURE_LEVELS], ["SES", "QES"]);
34
+ });
35
+
36
+ test("a template contract takes the form fields; an upload takes the file", async () => {
37
+ const template = Object.assign(new CreateDealContractDto(), {
38
+ source: "template",
39
+ deliverables: ["1 reel on Instagram"],
40
+ liveByAt: "2026-11-01T10:00:00.000Z",
41
+ licenceMonths: 6,
42
+ });
43
+ assert.deepEqual(await errorsOf(template), []);
44
+
45
+ const upload = Object.assign(new CreateDealContractDto(), { source: "uploaded", fileName: "umowa.pdf", fileBase64: "JVBERi0=" });
46
+ assert.deepEqual(await errorsOf(upload), []);
47
+
48
+ assert.deepEqual(await errorsOf(Object.assign(new CreateDealContractDto(), { source: "fax" })), ["source"]);
49
+ assert.deepEqual(await errorsOf(Object.assign(new CreateDealContractDto(), { source: "template", signatureLevel: "AES" })), ["signatureLevel"]);
50
+ assert.deepEqual(await errorsOf(Object.assign(new CreateDealContractDto(), { source: "template", licenceMonths: -1 })), ["licenceMonths"]);
51
+ });
52
+
53
+ test("a payment names its method or none (card by default; invoice for big sums)", async () => {
54
+ assert.deepEqual([...DEAL_PAYMENT_METHODS], ["card", "invoice"]);
55
+ assert.deepEqual(await errorsOf(new StartDealPaymentDto()), []);
56
+ assert.deepEqual(await errorsOf(Object.assign(new StartDealPaymentDto(), { method: "invoice" })), []);
57
+ assert.deepEqual(await errorsOf(Object.assign(new StartDealPaymentDto(), { method: "cash" })), ["method"]);
58
+ });
59
+
60
+ test("the payout profile: IBAN and payout currency, nothing about a provider", async () => {
61
+ const ok = Object.assign(new UpsertPayoutProfileDto(), { iban: "PL61109010140000071219812874", payoutCurrency: "PLN", nip: "5260250274" });
62
+ assert.deepEqual(await errorsOf(ok), []);
63
+ assert.deepEqual(await errorsOf(Object.assign(new UpsertPayoutProfileDto(), { iban: "not an iban", payoutCurrency: "PLN" })), ["iban"]);
64
+ assert.deepEqual(await errorsOf(Object.assign(new UpsertPayoutProfileDto(), { iban: "PL61109010140000071219812874", payoutCurrency: "zł" })), ["payoutCurrency"]);
65
+ assert.equal("stripeAccountId" in ok, false);
66
+ assert.deepEqual([...PAYOUT_PROFILE_FIELDS], ["settlement_form", "nip", "tax_statement", "intl_billing_data", "iban", "payout_currency"]);
67
+ });
68
+
69
+ test("the money acts a deal view offers its reader", () => {
70
+ assert.deepEqual([...DEAL_MONEY_ACTIONS], ["send_contract", "pay_deposit", "decline_deposit", "fund", "refund", "claim_guarantee"]);
71
+ });
72
+
73
+ test("the four wave-4 codes are added, never renamed", () => {
74
+ assert.equal(DEALROOM_CODES.DEAL_BELOW_MINIMUM, "dealroom-deal-below-minimum");
75
+ assert.equal(DEALROOM_CODES.MONEY_WRONG_STEP, "dealroom-money-wrong-step");
76
+ assert.equal(DEALROOM_CODES.CONTRACT_EXISTS, "dealroom-contract-exists");
77
+ assert.equal(DEALROOM_CODES.GUARANTEE_UNAVAILABLE, "dealroom-guarantee-unavailable");
78
+ });
79
+
80
+ test("contract, money and payout steps are kinds in 175's one event log (FR-008a)", () => {
81
+ for (const kind of DEALROOM_MONEY_EVENT_KINDS) {
82
+ assert.ok((AGENCY_CAMPAIGN_EVENT_KINDS as readonly string[]).includes(kind), kind);
83
+ assert.ok(kind.length <= 32, kind);
84
+ }
85
+ assert.deepEqual([...DEALROOM_MONEY_EVENT_KINDS], [
86
+ "contract_sent",
87
+ "contract_signed",
88
+ "contract_declined",
89
+ "deposit_paid",
90
+ "deal_funded",
91
+ "deal_closed_unviewed",
92
+ "deal_refund_requested",
93
+ "deal_refunded",
94
+ "guarantee_claimed",
95
+ "payout_initiated",
96
+ "payout_paid",
97
+ ]);
98
+ });
99
+
100
+ test("a payout profile with no payable settlement form names no billing variant", () => {
101
+ const profile: import("./dealroom").I_PayoutProfile = { billingVariant: null, track: null, payoutCurrency: "PLN", kycState: "none" };
102
+ assert.equal(profile.billingVariant, null);
103
+ });
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Spec 148 (Dealroom), T060: the additive types on top of 175's agency files.
3
+ *
4
+ * Two kinds of pin live here. The ADDITIONS (brand type, the `deals`
5
+ * delegation scope, application origins, the `apply` act, the Dealroom codes
6
+ * and the dormant money states) and the NON-CHANGES that make them safe: 175's
7
+ * offer status list and the scope set an organization may ask for stay
8
+ * byte-identical to 1.50.0.
9
+ */
10
+ import { test } from "node:test";
11
+ import assert from "node:assert/strict";
12
+
13
+ import {
14
+ AGENCY_GRANT_SCOPES,
15
+ AGENCY_ORGANIZATION_TYPES,
16
+ CREATOR_ACCESS_GRANT_ORIGINS,
17
+ DELEGATION_SCOPES,
18
+ isAgencyGrantScope,
19
+ isGrantScope,
20
+ } from "./agency";
21
+ import {
22
+ AGENCY_CAMPAIGN_MEMBER_ORIGINS,
23
+ AGENCY_OFFER_ACTIONS,
24
+ AGENCY_OFFER_STATUSES,
25
+ CampaignOfferActDto,
26
+ } from "./agency-campaigns";
27
+ import { I_OfferStatus } from "./I_Offer";
28
+ import {
29
+ CAMPAIGN_VISIBILITIES,
30
+ COMMISSION_AD_SPACE_DECISIONS,
31
+ COMMISSION_AD_SPACE_STATES,
32
+ COMMISSION_JOIN_STATES,
33
+ COMMISSION_LINK_MODES,
34
+ COMMISSION_LINK_STATES,
35
+ COMMISSION_MATERIAL_KINDS,
36
+ COMMISSION_NETWORKS,
37
+ ReviewCommissionAdSpaceDto,
38
+ COMMISSION_OFFER_STATES,
39
+ DEALROOM_CODES,
40
+ DEALROOM_PROFILE_FIELDS,
41
+ DEAL_MONEY_STATES,
42
+ DEAL_PAYOUT_STATES,
43
+ PAYOUT_BILLING_VARIANTS,
44
+ PAYOUT_LEDGER_SOURCES,
45
+ PAYOUT_LEDGER_STATES,
46
+ isCampaignVisibility,
47
+ } from "./dealroom";
48
+ import * as typesIndex from "./index";
49
+ import { EVENTS } from "../analytics";
50
+
51
+ // ── Non-changes (the 175 contract stays intact) ─────────────────────────────
52
+
53
+ test("AGENCY_OFFER_STATUSES is byte-identical to 1.50.0: no offer status is added or remodeled", () => {
54
+ assert.deepEqual(
55
+ [...AGENCY_OFFER_STATUSES],
56
+ [
57
+ I_OfferStatus.SENT,
58
+ I_OfferStatus.INFLUENCER_NEGOTIATED,
59
+ I_OfferStatus.AGENCY_NEGOTIATED,
60
+ I_OfferStatus.ACCEPTED,
61
+ I_OfferStatus.CONFIRMED,
62
+ I_OfferStatus.REJECTED,
63
+ I_OfferStatus.WITHDRAWN,
64
+ ],
65
+ );
66
+ assert.deepEqual(
67
+ Object.entries(I_OfferStatus),
68
+ [
69
+ ["DRAFT", "draft"],
70
+ ["SENT", "sent"],
71
+ ["DELETED", "deleted"],
72
+ ["ACCEPTED", "accepted"],
73
+ ["REJECTED", "rejected"],
74
+ ["INFLUENCER_NEGOTIATED", "influencerNegotiated"],
75
+ ["AGENCY_NEGOTIATED", "agencyNegotiated"],
76
+ ["CONFIRMED", "confirmed"],
77
+ ["ALTERNATIVE_SUGGESTED", "alternativeSuggested"],
78
+ ["WITHDRAWN", "withdrawn"],
79
+ ],
80
+ );
81
+ });
82
+
83
+ test("AGENCY_GRANT_SCOPES (what an organization may ask for) is unchanged and never carries `deals`", () => {
84
+ assert.deepEqual([...AGENCY_GRANT_SCOPES], ["analytics", "comments", "posts"]);
85
+ assert.equal(isAgencyGrantScope("deals"), false);
86
+ });
87
+
88
+ // ── Additions in 175's files ────────────────────────────────────────────────
89
+
90
+ test("an organization is an agency or a brand", () => {
91
+ assert.deepEqual([...AGENCY_ORGANIZATION_TYPES], ["agency", "brand"]);
92
+ });
93
+
94
+ test("a grant may originate from a Dealroom application", () => {
95
+ assert.deepEqual([...CREATOR_ACCESS_GRANT_ORIGINS], ["invite_link", "agency_request", "dealroom_application"]);
96
+ });
97
+
98
+ test("delegation is one creator-only scope, `deals`, kept out of the organization's ask", () => {
99
+ assert.deepEqual([...DELEGATION_SCOPES], ["deals"]);
100
+ for (const scope of [...AGENCY_GRANT_SCOPES, ...DELEGATION_SCOPES]) assert.equal(isGrantScope(scope), true);
101
+ for (const notAScope of ["publish", "payouts", "contract", "settings", "", "Deals"]) {
102
+ assert.equal(isGrantScope(notAScope), false, `must refuse: ${notAScope}`);
103
+ }
104
+ });
105
+
106
+ test("a campaign membership comes from an invitation or an application", () => {
107
+ assert.deepEqual([...AGENCY_CAMPAIGN_MEMBER_ORIGINS], ["invitation", "application"]);
108
+ });
109
+
110
+ test("`apply` is an offer act, appended after 175's four", () => {
111
+ assert.deepEqual([...AGENCY_OFFER_ACTIONS], ["accept", "counter", "confirm", "reject", "apply"]);
112
+ assert.equal(new CampaignOfferActDto({ action: "apply" }).action, "apply");
113
+ });
114
+
115
+ // ── dealroom.ts ─────────────────────────────────────────────────────────────
116
+
117
+ test("a campaign is private (every 175 campaign today) or open (listed in the feed)", () => {
118
+ assert.deepEqual([...CAMPAIGN_VISIBILITIES], ["private", "open"]);
119
+ assert.equal(isCampaignVisibility("open"), true);
120
+ assert.equal(isCampaignVisibility("public"), false);
121
+ });
122
+
123
+ test("the incomplete-profile state names the two fields that can be missing", () => {
124
+ assert.deepEqual([...DEALROOM_PROFILE_FIELDS], ["country", "niches"]);
125
+ });
126
+
127
+ test("DEALROOM_CODES are exactly the contract's thirteen, wave 4's four, the commission lane's four and review 148's three", () => {
128
+ assert.deepEqual(Object.values(DEALROOM_CODES).sort(), [
129
+ "commission-ad-space-decision-closed",
130
+ "commission-ad-space-pending",
131
+ "commission-ad-space-unavailable",
132
+ "commission-network-unavailable",
133
+ "dealroom-brand-door-closed",
134
+ "dealroom-brand-no-roster",
135
+ "dealroom-config-missing",
136
+ "dealroom-contract-exists",
137
+ "dealroom-contract-unsigned",
138
+ "dealroom-deal-below-minimum",
139
+ "dealroom-delegate-consent-forbidden",
140
+ "dealroom-delegate-forbidden",
141
+ "dealroom-delegate-own-deal",
142
+ "dealroom-deposit-required",
143
+ "dealroom-guarantee-unavailable",
144
+ "dealroom-listing-frozen",
145
+ "dealroom-logo-required",
146
+ "dealroom-money-disabled",
147
+ "dealroom-money-wrong-step",
148
+ "dealroom-near-match-not-appliable",
149
+ "dealroom-not-funded",
150
+ "dealroom-offer-filled",
151
+ "dealroom-profile-incomplete",
152
+ "dealroom-template-pending",
153
+ ]);
154
+ });
155
+
156
+ test("the deal money states mirror data-model's state machine exactly; payout is its own column", () => {
157
+ assert.deepEqual(
158
+ [...DEAL_MONEY_STATES],
159
+ [
160
+ "awaiting_material",
161
+ "material_submitted",
162
+ "deposit_paid",
163
+ "funded",
164
+ "approved",
165
+ "published",
166
+ "closed_unviewed",
167
+ "refund_pending",
168
+ "refunded",
169
+ "guarantee_claimed",
170
+ ],
171
+ );
172
+ assert.deepEqual([...DEAL_PAYOUT_STATES], ["none", "initiated", "paid"]);
173
+ });
174
+
175
+ test("payout profiles and the ledger carry the data-model's values", () => {
176
+ assert.deepEqual([...PAYOUT_BILLING_VARIANTS], ["faktura_vat", "umowa_o_dzielo", "international"]);
177
+ assert.deepEqual([...PAYOUT_LEDGER_SOURCES], ["deal", "pot", "commission"]);
178
+ assert.deepEqual([...PAYOUT_LEDGER_STATES], ["earned", "validated", "paid"]);
179
+ });
180
+
181
+ test("the commission lane carries the two networks and its own states", () => {
182
+ assert.deepEqual([...COMMISSION_NETWORKS], ["awin", "admitad"]);
183
+ assert.deepEqual([...COMMISSION_OFFER_STATES], ["live", "paused", "gone"]);
184
+ assert.deepEqual([...COMMISSION_LINK_STATES], ["active", "suspended"]);
185
+ assert.deepEqual([...COMMISSION_LINK_MODES], ["subnetwork", "deeplink"]);
186
+ });
187
+
188
+ // ── Commission lane, amended 2026-10-07 (Admitad Ad Space, Awin consent) ────
189
+
190
+ test("an Admitad Ad Space is reviewed, registered, rejected or suspended, never anything else", () => {
191
+ assert.deepEqual([...COMMISSION_AD_SPACE_STATES], ["pending_review", "registered", "rejected", "suspended"]);
192
+ assert.deepEqual([...COMMISSION_AD_SPACE_DECISIONS], ["approve", "reject", "suspend"]);
193
+ });
194
+
195
+ test("a join answers a link, a deep link, or an Ad Space under review", () => {
196
+ assert.deepEqual([...COMMISSION_JOIN_STATES], ["link", "deeplink", "ad_space_pending"]);
197
+ });
198
+
199
+ test("advertiser materials have named kinds", () => {
200
+ assert.deepEqual([...COMMISSION_MATERIAL_KINDS], ["logo", "banner", "landing_preview", "coupon"]);
201
+ });
202
+
203
+ test("ReviewCommissionAdSpaceDto carries the admin's decision", () => {
204
+ assert.equal(new ReviewCommissionAdSpaceDto({ decision: "approve" }).decision, "approve");
205
+ assert.equal(new ReviewCommissionAdSpaceDto().decision, undefined);
206
+ });
207
+
208
+ test("dealroom.ts is exported from the types index", () => {
209
+ assert.equal((typesIndex as Record<string, unknown>).DEALROOM_CODES, DEALROOM_CODES);
210
+ });
211
+
212
+ // ── Analytics: Dealroom-only events ─────────────────────────────────────────
213
+
214
+ test("Dealroom adds only the events 175 has none for", () => {
215
+ const dealroom = Object.entries(EVENTS)
216
+ .filter(([key]) => key.startsWith("DEALROOM_"))
217
+ .map(([, value]) => value)
218
+ .sort();
219
+ assert.deepEqual(dealroom, [
220
+ "dealroom_applied",
221
+ "dealroom_broaden_toggled",
222
+ "dealroom_connect_from_near_match_clicked",
223
+ "dealroom_deal_money_state_changed",
224
+ "dealroom_deal_payout_state_changed",
225
+ "dealroom_delegation_changed",
226
+ "dealroom_feed_viewed",
227
+ "dealroom_listing_opened",
228
+ "dealroom_near_match_promoted",
229
+ "dealroom_near_match_shown",
230
+ ]);
231
+ });