ad2app-lib 1.47.0 → 1.50.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.
- package/dist/analytics/index.d.ts +76 -0
- package/dist/analytics/index.js +29 -0
- package/dist/types/I_Offer.d.ts +3 -1
- package/dist/types/I_Offer.js +2 -0
- package/dist/types/agency-campaigns.d.ts +427 -0
- package/dist/types/agency-campaigns.js +479 -0
- package/dist/types/agency-pots.d.ts +518 -0
- package/dist/types/agency-pots.js +393 -0
- package/dist/types/agency-threads.d.ts +87 -0
- package/dist/types/agency-threads.js +60 -0
- package/dist/types/agency.d.ts +2 -0
- package/dist/types/agency.js +2 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +3 -0
- package/package.json +1 -1
- package/src/analytics/index.test.ts +52 -1
- package/src/analytics/index.ts +62 -0
- package/src/types/I_Offer.ts +2 -0
- package/src/types/agency-campaigns.test.ts +85 -0
- package/src/types/agency-campaigns.ts +739 -0
- package/src/types/agency-pots.test.ts +255 -0
- package/src/types/agency-pots.ts +751 -0
- package/src/types/agency-threads.ts +103 -0
- package/src/types/agency.ts +2 -0
- package/src/types/index.ts +3 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import { validateSync } from "class-validator";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ApplyToPotDto,
|
|
7
|
+
POT_CURRENCIES,
|
|
8
|
+
CREATOR_POT_FORBIDDEN_KEY,
|
|
9
|
+
DISCLOSURE_VISIBLE_CHARS,
|
|
10
|
+
POT_STEPS,
|
|
11
|
+
checkDisclosure,
|
|
12
|
+
creatorPotForbiddenKeys,
|
|
13
|
+
POT_REMOVAL_DECISIONS,
|
|
14
|
+
potBonusMinor,
|
|
15
|
+
potEarnings,
|
|
16
|
+
potViewsAtMax,
|
|
17
|
+
potCostPer1000Minor,
|
|
18
|
+
type I_CreatorPotView,
|
|
19
|
+
} from "./agency-pots";
|
|
20
|
+
import { AGENCY_CAMPAIGN_EVENT_KINDS, CreateAgencyCampaignDto } from "./agency-campaigns";
|
|
21
|
+
import { EVENTS } from "../analytics";
|
|
22
|
+
|
|
23
|
+
// ── checkDisclosure (FR-040) ────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
test("every UOKiK wording passes, with or without #, in any case", () => {
|
|
26
|
+
const passing = [
|
|
27
|
+
"reklama",
|
|
28
|
+
"#reklama Poranny bieg",
|
|
29
|
+
"REKLAMA: nowa kolekcja",
|
|
30
|
+
"materiał reklamowy",
|
|
31
|
+
"#materiałreklamowy",
|
|
32
|
+
"Materiał reklamowy marki X",
|
|
33
|
+
"współpraca reklamowa z @northlight",
|
|
34
|
+
"#współpracareklamowa",
|
|
35
|
+
"płatna współpraca",
|
|
36
|
+
"#płatnawspółpraca",
|
|
37
|
+
"post sponsorowany",
|
|
38
|
+
"#postsponsorowany",
|
|
39
|
+
// Without Polish diacritics, as people type on a phone.
|
|
40
|
+
"material reklamowy",
|
|
41
|
+
"wspolpraca reklamowa",
|
|
42
|
+
"platna wspolpraca",
|
|
43
|
+
];
|
|
44
|
+
for (const caption of passing) {
|
|
45
|
+
assert.equal(checkDisclosure(caption).result, "pass", caption);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("the four bare tags are weak, never accepted silently", () => {
|
|
50
|
+
for (const tag of ["#współpraca", "#ad", "#spons", "#promo", "#wspolpraca", "#AD"]) {
|
|
51
|
+
const check = checkDisclosure(`Nowa kolekcja jest super ${tag}`);
|
|
52
|
+
assert.equal(check.result, "weak", tag);
|
|
53
|
+
assert.equal(check.found, tag);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a caption with no disclosure at all is missing", () => {
|
|
58
|
+
assert.deepEqual(checkDisclosure("Poranny bieg w nowej kurtce"), { result: "missing", found: null, at: null });
|
|
59
|
+
assert.equal(checkDisclosure("").result, "missing");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a wording past the first 125 characters is not visible and does not pass", () => {
|
|
63
|
+
assert.equal(DISCLOSURE_VISIBLE_CHARS, 125, "FR-040: visible without expanding the caption");
|
|
64
|
+
const filler = "x".repeat(DISCLOSURE_VISIBLE_CHARS);
|
|
65
|
+
const check = checkDisclosure(`${filler} #reklama`);
|
|
66
|
+
assert.equal(check.result, "weak");
|
|
67
|
+
assert.equal(check.found, "#reklama");
|
|
68
|
+
assert.equal(check.at, DISCLOSURE_VISIBLE_CHARS + 1);
|
|
69
|
+
// The last visible position still passes.
|
|
70
|
+
const edge = `${"x".repeat(DISCLOSURE_VISIBLE_CHARS - "reklama".length)}reklama`;
|
|
71
|
+
assert.equal(edge.length, DISCLOSURE_VISIBLE_CHARS);
|
|
72
|
+
assert.equal(checkDisclosure(edge).result, "missing", "glued to a word is not the word");
|
|
73
|
+
assert.equal(checkDisclosure(`${"x ".repeat(59)}reklama`).result, "pass");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("a wording inside another word is not a disclosure", () => {
|
|
77
|
+
for (const caption of ["reklamacja przyjęta", "antyreklama", "#reklamacja", "#adidas", "#promocja", "#sponsor"]) {
|
|
78
|
+
assert.notEqual(checkDisclosure(caption).result, "pass", caption);
|
|
79
|
+
}
|
|
80
|
+
assert.equal(checkDisclosure("#adidas").result, "missing");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("a strong wording wins over a weak tag anywhere in the caption", () => {
|
|
84
|
+
assert.equal(checkDisclosure("#współpraca #reklama nowa kurtka").result, "pass");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ── potEarnings (FR-030, FR-032, FR-035) ───────────────────────────────────
|
|
88
|
+
|
|
89
|
+
test("earnings are the guarantee plus the rate times views, exact to the minor unit", () => {
|
|
90
|
+
// 15 000,00 zł + 70,00 zł per 1000 × 1 000 views = 15 070,00 zł
|
|
91
|
+
assert.equal(potEarnings({ guaranteeMinor: 1_500_000, ratePer1000Minor: 7_000, views: 1_000, maxMinor: 9_000_000 }), 1_507_000);
|
|
92
|
+
// 55,00 zł per 1000 × 845 300 views = 46 491,50 zł; plus 20 000,00 zł
|
|
93
|
+
assert.equal(potEarnings({ guaranteeMinor: 2_000_000, ratePer1000Minor: 5_500, views: 845_300, maxMinor: 9_000_000 }), 6_649_150);
|
|
94
|
+
// A fraction of a grosz is never paid: rounded down.
|
|
95
|
+
assert.equal(potBonusMinor(3_333, 1), 3);
|
|
96
|
+
assert.equal(potBonusMinor(1, 999), 0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("earnings never exceed the maximum", () => {
|
|
100
|
+
assert.equal(potEarnings({ guaranteeMinor: 1_500_000, ratePer1000Minor: 7_000, views: 1_420_000, maxMinor: 9_000_000 }), 9_000_000);
|
|
101
|
+
for (let i = 0; i < 2_000; i++) {
|
|
102
|
+
const maxMinor = 1 + Math.floor(Math.random() * 20_000_000);
|
|
103
|
+
const guaranteeMinor = Math.floor(Math.random() * maxMinor);
|
|
104
|
+
const ratePer1000Minor = Math.floor(Math.random() * 100_000);
|
|
105
|
+
const views = Math.floor(Math.random() * 50_000_000);
|
|
106
|
+
const earned = potEarnings({ guaranteeMinor, ratePer1000Minor, views, maxMinor });
|
|
107
|
+
assert.ok(Number.isInteger(earned));
|
|
108
|
+
assert.ok(earned <= maxMinor, `${earned} > ${maxMinor}`);
|
|
109
|
+
assert.ok(earned >= guaranteeMinor);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("views at the maximum are the exact count, rounded up", () => {
|
|
114
|
+
// (90 000 − 15 000) / 70 × 1000 = 1 071 428.57… → 1 071 429
|
|
115
|
+
const terms = { guaranteeMinor: 1_500_000, ratePer1000Minor: 7_000, maxMinor: 9_000_000 };
|
|
116
|
+
const atMax = potViewsAtMax(terms);
|
|
117
|
+
assert.equal(atMax, 1_071_429);
|
|
118
|
+
assert.equal(potEarnings({ ...terms, views: atMax }), 9_000_000);
|
|
119
|
+
assert.ok(potEarnings({ ...terms, views: atMax - 1 }) < 9_000_000);
|
|
120
|
+
assert.equal(potViewsAtMax({ ...terms, ratePer1000Minor: 0 }), null, "a zero rate never reaches the maximum");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── The creator's view carries nothing pot-level (SC-016) ───────────────────
|
|
124
|
+
|
|
125
|
+
test("I_CreatorPotView has no key matching a pot-level word", () => {
|
|
126
|
+
// A full sample of the type: every key the creator receives. Adding a key to
|
|
127
|
+
// the interface without adding it here fails to compile (the satisfies check).
|
|
128
|
+
const sample = {
|
|
129
|
+
campaignId: "c",
|
|
130
|
+
currency: "PLN",
|
|
131
|
+
maxPerCreatorMinor: 1,
|
|
132
|
+
bonusWindowDays: 30,
|
|
133
|
+
keepUpMonths: 12,
|
|
134
|
+
coverUrl: null,
|
|
135
|
+
applicationDeadlineAt: null,
|
|
136
|
+
availability: "full_max_available",
|
|
137
|
+
approx: { currency: "EUR", maxPerCreatorMinor: 1, rateDate: "2026-09-30" },
|
|
138
|
+
schedule: { materialsDueAt: null, publicationDueAt: null, bonusEndsAt: null, keepUpEndsAt: null },
|
|
139
|
+
publishing: { paidPartnershipLabel: true, sponsorHandle: null, collabHandle: null, firstComment: null, commentsEnabled: true },
|
|
140
|
+
entry: {
|
|
141
|
+
state: "accepted",
|
|
142
|
+
terms: { guaranteeMinor: 1, ratePer1000Minor: 1 },
|
|
143
|
+
appliedAt: "x",
|
|
144
|
+
decidedAt: null,
|
|
145
|
+
publishAt: null,
|
|
146
|
+
publishConfirmedAt: null,
|
|
147
|
+
publishedAt: null,
|
|
148
|
+
postUrl: null,
|
|
149
|
+
bonusEndsAt: null,
|
|
150
|
+
keepUpEndsAt: null,
|
|
151
|
+
outcome: null,
|
|
152
|
+
mayTakeDown: false,
|
|
153
|
+
earnings: { guaranteeMinor: 1, bonusMinor: 0, heldBonusMinor: 0, totalMinor: 1, views: 0, lastCountAt: null, frozen: false },
|
|
154
|
+
},
|
|
155
|
+
canApply: false,
|
|
156
|
+
canWithdraw: false,
|
|
157
|
+
canConfirmPublishAt: false,
|
|
158
|
+
medianViews: null,
|
|
159
|
+
} satisfies Required<I_CreatorPotView>;
|
|
160
|
+
assert.deepEqual(creatorPotForbiddenKeys(sample), []);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("the fence catches a pot-level key at any depth", () => {
|
|
164
|
+
assert.deepEqual(creatorPotForbiddenKeys({ a: { b: [{ potBudgetMinor: 1 }] } }), ["a.b.0.potBudgetMinor"]);
|
|
165
|
+
for (const key of ["budgetMinor", "remainderMinor", "reservedMinor", "fundedMinor", "max_budget", "maxBudgetMinor", "goalViews", "unreserved", "creatorsCoveredAtFullMax", "maxCommitmentMinor", "acceptedCreators"]) {
|
|
166
|
+
assert.ok(CREATOR_POT_FORBIDDEN_KEY.test(key), key);
|
|
167
|
+
}
|
|
168
|
+
// SC-016 names the earned amount; views and the blended cost are pot-level too (review 2026-10-06 M9).
|
|
169
|
+
for (const key of ["earnedMinor", "blendedCostPer1000Minor", "resultViews", "heldViews", "countedViews"]) {
|
|
170
|
+
assert.ok(CREATOR_POT_FORBIDDEN_KEY.test(key), key);
|
|
171
|
+
}
|
|
172
|
+
for (const key of ["maxPerCreatorMinor", "guaranteeMinor", "keepUpMonths", "availability", "acceptedAt", "decidedAt", "earnings", "bonusMinor", "totalMinor", "views", "medianViews"]) {
|
|
173
|
+
assert.ok(!CREATOR_POT_FORBIDDEN_KEY.test(key), key);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("a decomposed Polish letter, a double space or a no-break space still pass (review 2026-10-06 L9)", () => {
|
|
178
|
+
assert.equal(checkDisclosure("współpraca reklamowa".normalize("NFD")).result, "pass");
|
|
179
|
+
assert.equal(checkDisclosure("materiał reklamowy").result, "pass");
|
|
180
|
+
assert.equal(checkDisclosure("płatna\u00a0współpraca").result, "pass");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a tag glued to the word before it starts at its #, so the fold is judged from there", () => {
|
|
184
|
+
const caption = "x".repeat(DISCLOSURE_VISIBLE_CHARS - 2) + "x#reklama";
|
|
185
|
+
const check = checkDisclosure(caption);
|
|
186
|
+
assert.equal(check.at, DISCLOSURE_VISIBLE_CHARS - 1);
|
|
187
|
+
assert.equal(check.result, "pass");
|
|
188
|
+
assert.equal(check.found, "#reklama");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("the money helpers never throw on a non-whole input", () => {
|
|
192
|
+
assert.equal(potBonusMinor(Number.NaN, 1000), 0);
|
|
193
|
+
assert.equal(potBonusMinor(100, Number.POSITIVE_INFINITY), 0);
|
|
194
|
+
assert.equal(potViewsAtMax({ guaranteeMinor: 0, ratePer1000Minor: 0.5, maxMinor: 100 }), null);
|
|
195
|
+
assert.equal(potCostPer1000Minor(10.5, 1000), 10);
|
|
196
|
+
assert.equal(potCostPer1000Minor(100, Number.NaN), null);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ── States, events, DTOs ─────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
test("the pot board columns are the approved design's, in order", () => {
|
|
202
|
+
assert.deepEqual([...POT_STEPS], ["invited", "applied", "accepted", "material_in", "scheduled", "counting", "final", "done"]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("every pot act is an event kind, recorded with actor and amount (FR-021, FR-033)", () => {
|
|
206
|
+
for (const kind of [
|
|
207
|
+
"pot_applied",
|
|
208
|
+
"pot_application_withdrawn",
|
|
209
|
+
"pot_accepted",
|
|
210
|
+
"pot_declined",
|
|
211
|
+
"pot_released",
|
|
212
|
+
"pot_refilled",
|
|
213
|
+
"pot_moved",
|
|
214
|
+
"pot_deadline_moved",
|
|
215
|
+
"pot_publish_time_set",
|
|
216
|
+
"pot_publish_time_confirmed",
|
|
217
|
+
"pot_publish_refused",
|
|
218
|
+
"pot_published",
|
|
219
|
+
"pot_counted",
|
|
220
|
+
"pot_frozen",
|
|
221
|
+
"pot_bonus_held",
|
|
222
|
+
"pot_bonus_approved",
|
|
223
|
+
"pot_bonus_capped",
|
|
224
|
+
"pot_bonus_message",
|
|
225
|
+
"pot_kept",
|
|
226
|
+
"pot_broken",
|
|
227
|
+
"caption_edited",
|
|
228
|
+
"file_uploaded",
|
|
229
|
+
"file_removed",
|
|
230
|
+
]) {
|
|
231
|
+
assert.ok((AGENCY_CAMPAIGN_EVENT_KINDS as readonly string[]).includes(kind), kind);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("the pot analytics events are snake_case and carry the agency prefix", () => {
|
|
236
|
+
const pot = Object.entries(EVENTS).filter(([name]) => name.startsWith("AGENCY_POT_"));
|
|
237
|
+
assert.ok(pot.length >= 6);
|
|
238
|
+
for (const [, value] of pot) assert.match(value, /^agency_pot_[a-z_]+$/);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("an application constructs empty, as the validation pipe does", () => {
|
|
242
|
+
assert.doesNotThrow(() => new ApplyToPotDto());
|
|
243
|
+
assert.equal(new ApplyToPotDto({ guaranteeMinor: 5, ratePer1000Minor: 7 }).ratePer1000Minor, 7);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("the agency's decision on a removal after the bonus window is keep or withhold, nothing else", () => {
|
|
247
|
+
assert.deepEqual([...POT_REMOVAL_DECISIONS], ["keep_base", "withhold_base"]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("a pot is kept in a two-decimal currency only", () => {
|
|
251
|
+
const errors = (potCurrency: string) =>
|
|
252
|
+
validateSync(new CreateAgencyCampaignDto({ name: "x", kind: "pot", potCurrency })).filter((e) => e.property === "potCurrency");
|
|
253
|
+
for (const c of POT_CURRENCIES) assert.deepEqual(errors(c), [], c);
|
|
254
|
+
for (const c of ["JPY", "KWD", "pln", "XXX"]) assert.equal(errors(c).length, 1, c);
|
|
255
|
+
});
|