ad2app-lib 1.49.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.
- package/dist/analytics/index.d.ts +85 -2
- package/dist/analytics/index.js +26 -0
- package/dist/types/I_AccessDenial.d.ts +7 -0
- package/dist/types/I_AccessDenial.js +7 -0
- package/dist/types/agency-campaigns.d.ts +108 -5
- package/dist/types/agency-campaigns.js +111 -5
- package/dist/types/agency-pots.d.ts +518 -0
- package/dist/types/agency-pots.js +393 -0
- package/dist/types/agency-threads.d.ts +22 -1
- package/dist/types/agency.d.ts +93 -6
- package/dist/types/agency.js +70 -3
- package/dist/types/dealroom.d.ts +651 -0
- package/dist/types/dealroom.js +387 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +3 -0
- package/package.json +1 -1
- package/src/analytics/index.test.ts +23 -1
- package/src/analytics/index.ts +69 -2
- package/src/types/I_AccessDenial.ts +7 -0
- package/src/types/access-denial.test.ts +2 -1
- package/src/types/agency-campaigns.ts +187 -5
- package/src/types/agency-pots.test.ts +255 -0
- package/src/types/agency-pots.ts +751 -0
- package/src/types/agency-threads.ts +14 -3
- package/src/types/agency.ts +120 -7
- package/src/types/consumer-compat.test.ts +90 -0
- package/src/types/dealroom-audit1.test.ts +56 -0
- package/src/types/dealroom-wave2.test.ts +61 -0
- package/src/types/dealroom-wave3.test.ts +68 -0
- package/src/types/dealroom-wave4.test.ts +103 -0
- package/src/types/dealroom.test.ts +231 -0
- package/src/types/dealroom.ts +868 -0
- package/src/types/index.ts +4 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IsNotEmpty, IsString, Matches, MaxLength } from 'class-validator';
|
|
2
2
|
|
|
3
3
|
import type { CampaignSide, I_CampaignCreatorIdentity } from './agency-campaigns';
|
|
4
|
+
import type { AgencyOrganizationType } from './agency';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Agency ↔ creator threads (spec 175 US8, FR-023): one conversation per
|
|
@@ -36,15 +37,25 @@ export interface I_AgencyThreadMessage {
|
|
|
36
37
|
side: CampaignSide;
|
|
37
38
|
body: string;
|
|
38
39
|
at: string;
|
|
40
|
+
/** A delegate wrote this for the creator through a `deals` grant (spec 148): which grant, and the member's name. */
|
|
41
|
+
actingFor?: { grantId: string; memberName: string };
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export interface I_AgencyThreadSummary {
|
|
42
45
|
id: string;
|
|
43
46
|
/** Agency side: who the thread is with. */
|
|
44
47
|
creator: I_CampaignCreatorIdentity;
|
|
45
|
-
/** Creator side: which organization. */
|
|
46
|
-
organization: { id: string; name: string };
|
|
47
|
-
|
|
48
|
+
/** Creator side: which organization, and whether it is an agency or a brand (spec 148). */
|
|
49
|
+
organization: { id: string; name: string; type?: AgencyOrganizationType };
|
|
50
|
+
/** `actingFor`: a delegate wrote the last message for the creator (spec 148 US8), under this member's name. */
|
|
51
|
+
lastMessage: { side: CampaignSide; body: string; at: string; actingFor?: { memberName: string } } | null;
|
|
52
|
+
/**
|
|
53
|
+
* Spec 148 US8 (audit #2): the other organizations whose members read this
|
|
54
|
+
* conversation acting for the creator: a manager holding the creator's
|
|
55
|
+
* `deals` grant while this is a deal's thread, or one whose member already
|
|
56
|
+
* wrote in it. Empty when only the two parties see it.
|
|
57
|
+
*/
|
|
58
|
+
delegates?: Array<{ id: string; name: string }>;
|
|
48
59
|
/** The other side wrote last, seen from the caller's side. */
|
|
49
60
|
unanswered: boolean;
|
|
50
61
|
/** No live relationship: read-only for whoever still exists. */
|
package/src/types/agency.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
IsUUID,
|
|
12
12
|
Length,
|
|
13
13
|
Matches,
|
|
14
|
+
ValidateIf,
|
|
14
15
|
} from 'class-validator';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -87,10 +88,37 @@ export type I_AgencyGrantScope = (typeof AGENCY_GRANT_SCOPES)[number];
|
|
|
87
88
|
export const isAgencyGrantScope = (value: string): value is I_AgencyGrantScope =>
|
|
88
89
|
(AGENCY_GRANT_SCOPES as readonly string[]).includes(value);
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Delegation (spec 148 FR-017a): the one scope that reaches WRITES, and only
|
|
93
|
+
* the creator-side deal acts (apply, offer acts, materials, the deal thread).
|
|
94
|
+
*
|
|
95
|
+
* Kept OUT of AGENCY_GRANT_SCOPES on purpose: that list is what an
|
|
96
|
+
* organization may ASK for, and no organization ever asks for this. Only the
|
|
97
|
+
* creator adds it to `grantedScopes`, which makes it the one exception to
|
|
98
|
+
* "granted is a subset of requested". Publishing, contract signature,
|
|
99
|
+
* payouts, the settlement form and platforms stay unreachable through it.
|
|
100
|
+
*/
|
|
101
|
+
export const DELEGATION_SCOPES = ['deals'] as const;
|
|
102
|
+
|
|
103
|
+
export type I_DelegationScope = (typeof DELEGATION_SCOPES)[number];
|
|
104
|
+
|
|
105
|
+
/** Everything a grant may carry once the creator has answered. */
|
|
106
|
+
export type I_GrantScope = I_AgencyGrantScope | I_DelegationScope;
|
|
107
|
+
|
|
108
|
+
/** Narrowing guard over both sets; an unrecognised scope fails closed. */
|
|
109
|
+
export const isGrantScope = (value: string): value is I_GrantScope =>
|
|
110
|
+
isAgencyGrantScope(value) || (DELEGATION_SCOPES as readonly string[]).includes(value);
|
|
111
|
+
|
|
90
112
|
// ── Organization ─────────────────────────────────────────────────────────────
|
|
91
113
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
114
|
+
/**
|
|
115
|
+
* `'brand'` arrived with spec 148 (US2): one column, no enum table. A brand
|
|
116
|
+
* authors campaigns like an agency but has no roster; it is created only
|
|
117
|
+
* while the backend's brand door is open.
|
|
118
|
+
*/
|
|
119
|
+
export const AGENCY_ORGANIZATION_TYPES = ['agency', 'brand'] as const;
|
|
120
|
+
|
|
121
|
+
export type AgencyOrganizationType = (typeof AGENCY_ORGANIZATION_TYPES)[number];
|
|
94
122
|
|
|
95
123
|
/**
|
|
96
124
|
* `closed` was added by the 2026-09-21 requirements review (FR-004b).
|
|
@@ -134,6 +162,11 @@ export interface I_AgencyOrganization {
|
|
|
134
162
|
/** ISO-3166 alpha-2 of the register the identifier belongs to. */
|
|
135
163
|
registryCountry: string;
|
|
136
164
|
contactEmail: string;
|
|
165
|
+
/**
|
|
166
|
+
* The organization's logo, a storage path read through a signed URL (spec
|
|
167
|
+
* 148). Required before a campaign is listed open. Absent on older backends.
|
|
168
|
+
*/
|
|
169
|
+
logoUrl?: string;
|
|
137
170
|
status: AgencyOrganizationStatus;
|
|
138
171
|
/** The reason an admin gave; shown to a rejected applicant (FR-002). */
|
|
139
172
|
statusReason?: string;
|
|
@@ -182,7 +215,14 @@ export const CREATOR_ACCESS_GRANT_STATES = [
|
|
|
182
215
|
|
|
183
216
|
export type CreatorAccessGrantState = (typeof CREATOR_ACCESS_GRANT_STATES)[number];
|
|
184
217
|
|
|
185
|
-
|
|
218
|
+
/**
|
|
219
|
+
* `dealroom_application` (spec 148): created or widened when the creator
|
|
220
|
+
* applies to an open listing. The system revokes a grant of this origin, and
|
|
221
|
+
* only of this origin, when that deal ends.
|
|
222
|
+
*/
|
|
223
|
+
export const CREATOR_ACCESS_GRANT_ORIGINS = ['invite_link', 'agency_request', 'dealroom_application'] as const;
|
|
224
|
+
|
|
225
|
+
export type CreatorAccessGrantOrigin = (typeof CREATOR_ACCESS_GRANT_ORIGINS)[number];
|
|
186
226
|
|
|
187
227
|
/** Who ended it. Shown in the agency's past-creators history (FR-013b). */
|
|
188
228
|
export type CreatorAccessGrantRevokedBy =
|
|
@@ -203,10 +243,19 @@ export interface I_CreatorAccessGrant {
|
|
|
203
243
|
creatorUserId?: string;
|
|
204
244
|
organizationId: string;
|
|
205
245
|
organizationName: string;
|
|
246
|
+
/** Spec 148: `brand` cannot hold `deals` (it is the creator's counterparty), so the switch is not offered. Absent on older backends. */
|
|
247
|
+
organizationType?: AgencyOrganizationType;
|
|
206
248
|
/** What the agency asked for. */
|
|
207
249
|
requestedScopes: I_AgencyGrantScope[];
|
|
208
|
-
/** What the creator actually granted — a subset, possibly smaller, never larger. */
|
|
250
|
+
/** What the creator actually granted — a subset, possibly smaller, never larger. Reads only. */
|
|
209
251
|
grantedScopes: I_AgencyGrantScope[];
|
|
252
|
+
/**
|
|
253
|
+
* Spec 148 FR-017a: the creator lets this organization run their Dealroom
|
|
254
|
+
* deals (the `deals` delegation scope, which no organization can ask for and
|
|
255
|
+
* only the creator adds). Kept out of `grantedScopes` so that list stays the
|
|
256
|
+
* reads 175 code is typed on. Absent on older backends: read absent as no.
|
|
257
|
+
*/
|
|
258
|
+
deals?: boolean;
|
|
210
259
|
/** Frozen at grant time; what the history shows after a purge. */
|
|
211
260
|
creatorIdentity: { handle: string; displayName?: string };
|
|
212
261
|
state: CreatorAccessGrantState;
|
|
@@ -357,6 +406,12 @@ export interface I_AgencyRosterRow {
|
|
|
357
406
|
*/
|
|
358
407
|
reaskedAt?: string;
|
|
359
408
|
reaskAvailableAt?: string;
|
|
409
|
+
/**
|
|
410
|
+
* Spec 148 US8: the organization may act for this creator in the Dealroom
|
|
411
|
+
* (the active grant holds `deals`, the org is not a brand). Only
|
|
412
|
+
* sent while the Dealroom is on; absent while it is off (read absent as no).
|
|
413
|
+
*/
|
|
414
|
+
canActForCreator?: boolean;
|
|
360
415
|
}
|
|
361
416
|
|
|
362
417
|
/**
|
|
@@ -534,7 +589,24 @@ export class CreateAgencyOrganizationDto {
|
|
|
534
589
|
@IsEmail()
|
|
535
590
|
contactEmail: string;
|
|
536
591
|
|
|
592
|
+
/** Spec 148 US2: absent = `agency`. `brand` is refused while the backend's brand door is closed. */
|
|
593
|
+
@IsOptional()
|
|
594
|
+
@IsIn(AGENCY_ORGANIZATION_TYPES as unknown as string[])
|
|
595
|
+
type?: AgencyOrganizationType;
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Spec 148 US2, money release only (DORMANT): the card a brand pays its
|
|
599
|
+
* first subscription charge with, taken BEFORE the organization exists.
|
|
600
|
+
* Ignored while the backend's money switch is off (brands apply free).
|
|
601
|
+
*/
|
|
602
|
+
@IsOptional()
|
|
603
|
+
@IsString()
|
|
604
|
+
@Matches(/^pm_[A-Za-z0-9]{1,200}$/)
|
|
605
|
+
paymentMethodId?: string;
|
|
606
|
+
|
|
537
607
|
constructor(data?: Partial<CreateAgencyOrganizationDto>) {
|
|
608
|
+
this.type = data?.type;
|
|
609
|
+
this.paymentMethodId = data?.paymentMethodId;
|
|
538
610
|
this.name = data?.name ?? '';
|
|
539
611
|
this.website = data?.website;
|
|
540
612
|
this.registryId = data?.registryId ?? '';
|
|
@@ -600,18 +672,44 @@ export class AcceptGrantDto {
|
|
|
600
672
|
}
|
|
601
673
|
}
|
|
602
674
|
|
|
603
|
-
/**
|
|
675
|
+
/**
|
|
676
|
+
* The creator narrowing or widening a live grant — the only other write path.
|
|
677
|
+
*
|
|
678
|
+
* `scopes` are the READS only; `deals` is never one of them (it stays out of
|
|
679
|
+
* AGENCY_GRANT_SCOPES). Delegation (spec 148 FR-017a) is its own field, so the
|
|
680
|
+
* data-access screen, which saves reads, can never drop it by omission:
|
|
681
|
+
*
|
|
682
|
+
* - `deals` absent ⇒ delegation unchanged (a reads-only save keeps `deals`);
|
|
683
|
+
* - `deals: true` ⇒ added, `deals: false` ⇒ removed;
|
|
684
|
+
* - `scopes` may be left out when `deals` is sent ⇒ the reads stay as they are.
|
|
685
|
+
*/
|
|
604
686
|
export class UpdateGrantScopesDto {
|
|
687
|
+
@ValidateIf((dto: UpdateGrantScopesDto) => dto.deals === undefined || dto.scopes !== undefined)
|
|
605
688
|
@IsArray()
|
|
606
689
|
@ArrayNotEmpty()
|
|
607
690
|
@IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
|
|
608
|
-
scopes
|
|
691
|
+
scopes?: I_AgencyGrantScope[];
|
|
692
|
+
|
|
693
|
+
/** Let this organization run my deals (spec 148 US8). Creator-only; absent = unchanged. */
|
|
694
|
+
@IsOptional()
|
|
695
|
+
@IsBoolean()
|
|
696
|
+
deals?: boolean;
|
|
609
697
|
|
|
610
698
|
constructor(data?: Partial<UpdateGrantScopesDto>) {
|
|
611
|
-
|
|
699
|
+
// Undefined when not given (never []): class-transformer builds the body
|
|
700
|
+
// with an empty construct, and a default would make `{ deals: true }` fail.
|
|
701
|
+
if (data?.scopes !== undefined) this.scopes = data.scopes;
|
|
702
|
+
if (data?.deals !== undefined) this.deals = data.deals;
|
|
612
703
|
}
|
|
613
704
|
}
|
|
614
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Spec 148 US8: the header a delegate names the creator with on the
|
|
708
|
+
* creator-side deal routes (`X-Acting-For: <creatorUserId>`). Every other
|
|
709
|
+
* route refuses a request that carries it (`dealroom-delegate-forbidden`).
|
|
710
|
+
*/
|
|
711
|
+
export const ACTING_FOR_HEADER = 'x-acting-for';
|
|
712
|
+
|
|
615
713
|
export class SponsorCreatorDto {
|
|
616
714
|
@IsIn(['starter', 'pro'])
|
|
617
715
|
tier: SponsoredTier;
|
|
@@ -695,6 +793,21 @@ export interface I_AgencyDiscoverResult {
|
|
|
695
793
|
results: I_AgencyDiscoverHit[];
|
|
696
794
|
}
|
|
697
795
|
|
|
796
|
+
/** `GET /admin/agency-organizations[?type=]`: one pending organization on the admin queue. */
|
|
797
|
+
export interface I_AdminOrganizationRow {
|
|
798
|
+
id: string;
|
|
799
|
+
/** Spec 148: `agency` or `brand`; filter with `?type=`. */
|
|
800
|
+
type: AgencyOrganizationType;
|
|
801
|
+
name: string;
|
|
802
|
+
website: string | null;
|
|
803
|
+
registryId: string;
|
|
804
|
+
registryCountry: string;
|
|
805
|
+
contactEmail: string;
|
|
806
|
+
/** Derived, a SIGNAL only (FR-003). */
|
|
807
|
+
contactEmailDomain: string;
|
|
808
|
+
appliedAt: string;
|
|
809
|
+
}
|
|
810
|
+
|
|
698
811
|
export class AdminSetOrganizationStatusDto {
|
|
699
812
|
@IsIn(['approved', 'rejected'])
|
|
700
813
|
status: Extract<AgencyOrganizationStatus, 'approved' | 'rejected'>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review #5 (H4): this minor must not break code written against 1.50.0. The
|
|
3
|
+
* fixture below is how the backend's and the frontend's master use the grant
|
|
4
|
+
* types (grant-view.ts, AgencyJoinPage, AgencyRequestPage, DataAccessSection);
|
|
5
|
+
* it type-checks against the current sources with the real compiler, so a
|
|
6
|
+
* widening that breaks a consumer fails here, not after the publish.
|
|
7
|
+
*/
|
|
8
|
+
import { test } from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join, resolve } from "node:path";
|
|
13
|
+
import ts from "typescript";
|
|
14
|
+
|
|
15
|
+
const TYPES = resolve(__dirname, "index").replace(/\\/g, "/");
|
|
16
|
+
|
|
17
|
+
const FIXTURE = `
|
|
18
|
+
import {
|
|
19
|
+
AGENCY_GRANT_SCOPES,
|
|
20
|
+
type I_AgencyGrantScope,
|
|
21
|
+
type I_CreatorAccessGrant,
|
|
22
|
+
type I_AgencyRosterRow,
|
|
23
|
+
type CreatorAccessGrantOrigin,
|
|
24
|
+
type AgencyOrganizationType,
|
|
25
|
+
UpdateGrantScopesDto,
|
|
26
|
+
} from "${TYPES}";
|
|
27
|
+
|
|
28
|
+
declare const grant: I_CreatorAccessGrant;
|
|
29
|
+
declare const row: I_AgencyRosterRow;
|
|
30
|
+
declare const reask: { scopes: I_AgencyGrantScope[] } | null;
|
|
31
|
+
const inCanonicalOrder = (scopes: readonly I_AgencyGrantScope[]): I_AgencyGrantScope[] =>
|
|
32
|
+
AGENCY_GRANT_SCOPES.filter((s) => scopes.includes(s));
|
|
33
|
+
const word = (scope: I_AgencyGrantScope): string => scope;
|
|
34
|
+
|
|
35
|
+
// frontend master: AgencyJoinPage:177, AgencyRequestPage:91/190, DataAccessSection:244
|
|
36
|
+
(grant.grantedScopes ?? []).map((scope) => word(scope));
|
|
37
|
+
inCanonicalOrder(grant.grantedScopes ?? []);
|
|
38
|
+
const initial: I_AgencyGrantScope[] = grant.grantedScopes ?? [];
|
|
39
|
+
const missing = AGENCY_GRANT_SCOPES.filter((s) => !row.grantedScopes.includes(s));
|
|
40
|
+
|
|
41
|
+
// backend master: grant-view.ts builds the row from 175's columns
|
|
42
|
+
declare const columns: { granted: I_AgencyGrantScope[]; requested: I_AgencyGrantScope[]; origin: 'invite_link' | 'agency_request' };
|
|
43
|
+
const built: I_CreatorAccessGrant = {
|
|
44
|
+
id: "g", organizationId: "o", organizationName: "O",
|
|
45
|
+
requestedScopes: columns.requested,
|
|
46
|
+
grantedScopes: columns.granted,
|
|
47
|
+
creatorIdentity: { handle: "h" },
|
|
48
|
+
state: "active",
|
|
49
|
+
origin: columns.origin,
|
|
50
|
+
platformsReached: [],
|
|
51
|
+
...(reask ? { reaskedScopes: reask.scopes, reaskExpiresAt: "x" } : {}),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// backend master: the scope update reads dto.scopes as the new reads
|
|
55
|
+
declare const dto: UpdateGrantScopesDto;
|
|
56
|
+
const next: I_AgencyGrantScope[] = dto?.scopes;
|
|
57
|
+
|
|
58
|
+
const origin: CreatorAccessGrantOrigin = "agency_request";
|
|
59
|
+
const type: AgencyOrganizationType = "agency";
|
|
60
|
+
export { initial, missing, built, next, origin, type };
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
test("1.50.0 consumer code type-checks against this version (no breaking widening)", () => {
|
|
64
|
+
const dir = mkdtempSync(join(tmpdir(), "lib-compat-"));
|
|
65
|
+
const file = join(dir, "consumer.ts");
|
|
66
|
+
writeFileSync(file, FIXTURE);
|
|
67
|
+
const program = ts.createProgram([file], {
|
|
68
|
+
// Both consumers compile with strict off (backend: unset, frontend: false).
|
|
69
|
+
strict: false,
|
|
70
|
+
noEmit: true,
|
|
71
|
+
target: ts.ScriptTarget.ES2020,
|
|
72
|
+
module: ts.ModuleKind.CommonJS,
|
|
73
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
74
|
+
experimentalDecorators: true,
|
|
75
|
+
emitDecoratorMetadata: true,
|
|
76
|
+
esModuleInterop: true,
|
|
77
|
+
skipLibCheck: true,
|
|
78
|
+
});
|
|
79
|
+
const errors = ts
|
|
80
|
+
.getPreEmitDiagnostics(program)
|
|
81
|
+
.filter((d) => d.file?.fileName === file)
|
|
82
|
+
.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n").split("\n")[0]);
|
|
83
|
+
assert.deepEqual(errors, []);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("delegation rides in its own field, never in grantedScopes", () => {
|
|
87
|
+
const grant: Pick<import("./agency").I_CreatorAccessGrant, "grantedScopes" | "deals"> = { grantedScopes: ["analytics"], deals: true };
|
|
88
|
+
assert.equal(grant.deals, true);
|
|
89
|
+
assert.deepEqual(grant.grantedScopes, ["analytics"]);
|
|
90
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec 148 audit #1: the additive wire fields the fixes need. Pinned
|
|
3
|
+
* so a rename is a decision: the terms-version refusal code the frontend
|
|
4
|
+
* words, and the optional fields an older backend simply leaves out.
|
|
5
|
+
*/
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
|
|
9
|
+
import { ACCESS_DENIAL_CODES } from "./I_AccessDenial";
|
|
10
|
+
import type { I_AgencyCampaignEvent, I_CreatorCampaignView, I_CreatorDelegateAct } from "./agency-campaigns";
|
|
11
|
+
import type { I_DealContract, I_DealroomListing } from "./dealroom";
|
|
12
|
+
import type { I_CreatorAccessGrant } from "./agency";
|
|
13
|
+
|
|
14
|
+
test("an unset terms version is a coded refusal, never a 500", () => {
|
|
15
|
+
assert.equal(ACCESS_DENIAL_CODES.AGENCY_TERMS_VERSION_MISSING, "agency-terms-version-missing");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("the listing names its campaign; an older backend may leave it out", () => {
|
|
19
|
+
const named: Pick<I_DealroomListing, "campaignName"> = { campaignName: "Poranna rutyna" };
|
|
20
|
+
const older: Pick<I_DealroomListing, "campaignName"> = {};
|
|
21
|
+
assert.equal(named.campaignName, "Poranna rutyna");
|
|
22
|
+
assert.equal(older.campaignName, undefined);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("a contract carries the caller's own signing link, when there is one", () => {
|
|
26
|
+
const mine: Pick<I_DealContract, "signingUrl"> = { signingUrl: "https://sign.example.test/s/1" };
|
|
27
|
+
const none: Pick<I_DealContract, "signingUrl"> = { signingUrl: null };
|
|
28
|
+
assert.ok(mine.signingUrl);
|
|
29
|
+
assert.equal(none.signingUrl, null);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("an application's events are marked as such, not as a note", () => {
|
|
33
|
+
const event: I_AgencyCampaignEvent = { kind: "joined", side: "creator", at: "2026-10-07T10:00:00.000Z", fromApplication: true };
|
|
34
|
+
assert.equal(event.fromApplication, true);
|
|
35
|
+
assert.equal(event.note, undefined);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("the creator's own view lists what a manager did for them", () => {
|
|
39
|
+
const act: I_CreatorDelegateAct = { kind: "material_uploaded", at: "2026-10-07T10:00:00.000Z", memberName: "Ola", organizationName: "Demo Talent Studio" };
|
|
40
|
+
const view: Pick<I_CreatorCampaignView, "delegateActs"> = { delegateActs: [act] };
|
|
41
|
+
assert.equal(view.delegateActs?.[0].memberName, "Ola");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a grant names its organization's type, so a brand is never offered `deals`", () => {
|
|
45
|
+
const grant: Pick<I_CreatorAccessGrant, "organizationType"> = { organizationType: "brand" };
|
|
46
|
+
assert.equal(grant.organizationType, "brand");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("review #14/#28: money-off, an unset value, a waiting template and a missing logo are four codes", async () => {
|
|
50
|
+
const { DEALROOM_CODES } = await import("./dealroom");
|
|
51
|
+
assert.equal(DEALROOM_CODES.MONEY_DISABLED, "dealroom-money-disabled");
|
|
52
|
+
assert.equal(DEALROOM_CODES.CONFIG_MISSING, "dealroom-config-missing");
|
|
53
|
+
assert.equal(DEALROOM_CODES.TEMPLATE_PENDING, "dealroom-template-pending");
|
|
54
|
+
assert.equal(DEALROOM_CODES.LOGO_REQUIRED, "dealroom-logo-required");
|
|
55
|
+
assert.equal(new Set(Object.values(DEALROOM_CODES)).size, Object.values(DEALROOM_CODES).length);
|
|
56
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec 148 wave 2 (T023-T027, T063): the wire shapes the listing, the brand
|
|
3
|
+
* door and the apply route read. Validated through class-validator, the way
|
|
4
|
+
* the backend's global ValidationPipe reads them.
|
|
5
|
+
*/
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
|
|
9
|
+
import { AdminSetOrganizationStatusDto, CreateAgencyOrganizationDto } from "./agency";
|
|
10
|
+
import { CreateAgencyCampaignDto, UpdateAgencyCampaignDto } from "./agency-campaigns";
|
|
11
|
+
import { ApplyToDealroomListingDto, DEALROOM_LISTING_STATES } 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);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const org = { name: "Marka", registryId: "1", registryCountry: "PL", contactEmail: "a@b.pl" };
|
|
19
|
+
|
|
20
|
+
test("an organization's type is optional (agency) and only agency or brand", async () => {
|
|
21
|
+
assert.deepEqual(await errorsOf(new CreateAgencyOrganizationDto(org)), []);
|
|
22
|
+
assert.deepEqual(await errorsOf(new CreateAgencyOrganizationDto({ ...org, type: "brand" })), []);
|
|
23
|
+
assert.deepEqual(await errorsOf(new CreateAgencyOrganizationDto({ ...org, type: "school" as never })), ["type"]);
|
|
24
|
+
assert.ok(new AdminSetOrganizationStatusDto());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("the listing fields ride on 175's campaign bodies, each optional and checked", async () => {
|
|
28
|
+
assert.deepEqual(await errorsOf(new CreateAgencyCampaignDto({ name: "X" })), []);
|
|
29
|
+
assert.deepEqual(
|
|
30
|
+
await errorsOf(
|
|
31
|
+
new UpdateAgencyCampaignDto({
|
|
32
|
+
visibility: "open",
|
|
33
|
+
targetProfile: { platforms: ["tiktok"], followerBand: {}, niches: ["beauty"] },
|
|
34
|
+
slots: 3,
|
|
35
|
+
applicationDeadline: "2026-12-01T00:00:00.000Z",
|
|
36
|
+
closedAt: "2026-11-01T00:00:00.000Z",
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
[],
|
|
40
|
+
);
|
|
41
|
+
assert.deepEqual(
|
|
42
|
+
(await errorsOf(new UpdateAgencyCampaignDto({ visibility: "public" as never, slots: 0, applicationDeadline: "soon" }))).sort(),
|
|
43
|
+
["applicationDeadline", "slots", "visibility"],
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("an application names at least one scope, from the reads only", async () => {
|
|
48
|
+
assert.deepEqual(await errorsOf(new ApplyToDealroomListingDto({ scopes: ["analytics"], priceMinor: 100, currency: "PLN" })), []);
|
|
49
|
+
assert.deepEqual(await errorsOf(new ApplyToDealroomListingDto({ scopes: [] })), ["scopes"]);
|
|
50
|
+
assert.deepEqual(await errorsOf(new ApplyToDealroomListingDto({ scopes: ["deals" as never] })), ["scopes"]);
|
|
51
|
+
assert.deepEqual(await errorsOf(new ApplyToDealroomListingDto({ scopes: ["posts"], currency: "zł" })), ["currency"]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("the derived listing states, in order", () => {
|
|
55
|
+
assert.deepEqual([...DEALROOM_LISTING_STATES], ["draft", "live", "frozen", "closed"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a brand's first-charge card is optional and a Stripe payment method id only (dormant)", async () => {
|
|
59
|
+
assert.deepEqual(await errorsOf(new CreateAgencyOrganizationDto({ ...org, type: "brand", paymentMethodId: "pm_123abc" })), []);
|
|
60
|
+
assert.deepEqual(await errorsOf(new CreateAgencyOrganizationDto({ ...org, type: "brand", paymentMethodId: "4242424242424242" })), ["paymentMethodId"]);
|
|
61
|
+
});
|
|
@@ -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
|
+
});
|