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.
- package/dist/analytics/index.d.ts +53 -2
- package/dist/analytics/index.js +14 -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 +70 -3
- package/dist/types/agency-campaigns.js +56 -3
- 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 +1 -0
- package/dist/types/index.js +2 -0
- package/package.json +1 -1
- package/src/analytics/index.test.ts +11 -0
- package/src/analytics/index.ts +46 -2
- package/src/types/I_AccessDenial.ts +7 -0
- package/src/types/access-denial.test.ts +2 -1
- package/src/types/agency-campaigns.ts +108 -2
- 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 +3 -0
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
IsIn,
|
|
7
7
|
IsInt,
|
|
8
8
|
IsNotEmpty,
|
|
9
|
+
IsObject,
|
|
9
10
|
IsOptional,
|
|
10
11
|
IsString,
|
|
11
12
|
Matches,
|
|
@@ -18,6 +19,7 @@ import { I_MediaStage } from './I_Media';
|
|
|
18
19
|
import { I_OfferStatus } from './I_Offer';
|
|
19
20
|
import { AGENCY_GRANT_SCOPES, type I_AgencyGrantScope } from './agency';
|
|
20
21
|
import { PotFields, type I_AgencyPotView, type I_CampaignFile, type I_CreatorPotView, type I_DisclosureCheck } from './agency-pots';
|
|
22
|
+
import { CAMPAIGN_VISIBILITIES, type CampaignVisibility, type I_CampaignListing, type I_TargetProfile } from './dealroom';
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* The campaign loop (spec 175 US7) — an organization runs a campaign with the
|
|
@@ -80,6 +82,10 @@ export type AgencyCampaignKind = (typeof AGENCY_CAMPAIGN_KINDS)[number];
|
|
|
80
82
|
export const AGENCY_CAMPAIGN_MEMBER_STATES = ['invited', 'joined', 'declined', 'left', 'removed'] as const;
|
|
81
83
|
export type AgencyCampaignMemberState = (typeof AGENCY_CAMPAIGN_MEMBER_STATES)[number];
|
|
82
84
|
|
|
85
|
+
/** How a creator came onto a campaign: 175's invitation, or a Dealroom application to an open listing (spec 148). */
|
|
86
|
+
export const AGENCY_CAMPAIGN_MEMBER_ORIGINS = ['invitation', 'application'] as const;
|
|
87
|
+
export type AgencyCampaignMemberOrigin = (typeof AGENCY_CAMPAIGN_MEMBER_ORIGINS)[number];
|
|
88
|
+
|
|
83
89
|
/** The offer statuses the agency loop writes. `draft`, `deleted` and `alternativeSuggested` are legacy-only. */
|
|
84
90
|
export const AGENCY_OFFER_STATUSES = [
|
|
85
91
|
I_OfferStatus.SENT,
|
|
@@ -99,9 +105,13 @@ export type CampaignSide = 'agency' | 'creator';
|
|
|
99
105
|
* right now is the server's `availableActions`, never a client guess.
|
|
100
106
|
*
|
|
101
107
|
* agency: counter · confirm · reject
|
|
102
|
-
* creator: accept · counter · reject
|
|
108
|
+
* creator: accept · counter · reject · apply
|
|
109
|
+
*
|
|
110
|
+
* `apply` (spec 148) is the creator's opening price on an open listing, with
|
|
111
|
+
* no offer yet: it lands in the existing negotiating status, the agency's
|
|
112
|
+
* turn. No offer status is added for it.
|
|
103
113
|
*/
|
|
104
|
-
export const AGENCY_OFFER_ACTIONS = ['accept', 'counter', 'confirm', 'reject'] as const;
|
|
114
|
+
export const AGENCY_OFFER_ACTIONS = ['accept', 'counter', 'confirm', 'reject', 'apply'] as const;
|
|
105
115
|
export type AgencyOfferAction = (typeof AGENCY_OFFER_ACTIONS)[number];
|
|
106
116
|
|
|
107
117
|
/** The agency's acts on the latest material; the creator's only act is to upload. */
|
|
@@ -178,6 +188,20 @@ export const AGENCY_CAMPAIGN_EVENT_KINDS = [
|
|
|
178
188
|
'caption_edited',
|
|
179
189
|
'file_uploaded',
|
|
180
190
|
'file_removed',
|
|
191
|
+
// Spec 148 money release (DORMANT): the contract, money and payout steps of a
|
|
192
|
+
// Dealroom deal are kinds in this one log (FR-008a). Mirrored by
|
|
193
|
+
// DEALROOM_MONEY_EVENT_KINDS in dealroom.ts.
|
|
194
|
+
'contract_sent',
|
|
195
|
+
'contract_signed',
|
|
196
|
+
'contract_declined',
|
|
197
|
+
'deposit_paid',
|
|
198
|
+
'deal_funded',
|
|
199
|
+
'deal_closed_unviewed',
|
|
200
|
+
'deal_refund_requested',
|
|
201
|
+
'deal_refunded',
|
|
202
|
+
'guarantee_claimed',
|
|
203
|
+
'payout_initiated',
|
|
204
|
+
'payout_paid',
|
|
181
205
|
] as const;
|
|
182
206
|
export type AgencyCampaignEventKind = (typeof AGENCY_CAMPAIGN_EVENT_KINDS)[number];
|
|
183
207
|
|
|
@@ -279,6 +303,21 @@ export interface I_AgencyCampaignEvent {
|
|
|
279
303
|
/** A pot act's amount: a refill, a move, a reserved guarantee, an accrued bonus. */
|
|
280
304
|
amount?: I_Money;
|
|
281
305
|
note?: string;
|
|
306
|
+
/** Set when a delegate acted on the creator's side through a `deals` grant (spec 148 FR-017a). */
|
|
307
|
+
actingGrantId?: string;
|
|
308
|
+
/** With `actingGrantId`: the member who acted, shown as "<member> for <creator>". */
|
|
309
|
+
actingMemberName?: string;
|
|
310
|
+
/** The act came from the creator's Dealroom application (spec 148 US4), not an invitation. */
|
|
311
|
+
fromApplication?: true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Spec 148 US8 AC3: one act a manager did in the creator's deal, for the creator's own view. */
|
|
315
|
+
export interface I_CreatorDelegateAct {
|
|
316
|
+
kind: AgencyCampaignEventKind;
|
|
317
|
+
at: string;
|
|
318
|
+
/** The member of the managing agency who acted. */
|
|
319
|
+
memberName: string;
|
|
320
|
+
organizationName: string;
|
|
282
321
|
}
|
|
283
322
|
|
|
284
323
|
export interface I_CampaignCreatorIdentity {
|
|
@@ -293,6 +332,8 @@ export interface I_AgencyCampaignMemberRow {
|
|
|
293
332
|
memberId: string;
|
|
294
333
|
creator: I_CampaignCreatorIdentity;
|
|
295
334
|
state: AgencyCampaignMemberState;
|
|
335
|
+
/** Invitation or Dealroom application (spec 148). Absent on older backends: read as 'invitation'. */
|
|
336
|
+
origin?: AgencyCampaignMemberOrigin;
|
|
296
337
|
/** The board column, or the reason the row is off the board. */
|
|
297
338
|
step: AgencyCampaignStep | AgencyCampaignOffBoard;
|
|
298
339
|
/** When the row reached its current step. */
|
|
@@ -346,6 +387,8 @@ export interface I_AgencyCampaign {
|
|
|
346
387
|
milestones: I_AgencyMilestone[];
|
|
347
388
|
pipeline: I_AgencyCampaignPipeline;
|
|
348
389
|
createdAt: string;
|
|
390
|
+
/** Spec 148: the Dealroom listing fields and their derived state. Absent on older backends. */
|
|
391
|
+
listing?: I_CampaignListing;
|
|
349
392
|
}
|
|
350
393
|
|
|
351
394
|
/** Opening one creator (FR-024): their course with dates, notes, files and milestones. */
|
|
@@ -355,6 +398,31 @@ export interface I_AgencyCampaignCreatorDetail {
|
|
|
355
398
|
events: I_AgencyCampaignEvent[];
|
|
356
399
|
materials: I_AgencyMaterial[];
|
|
357
400
|
milestones: I_AgencyMilestone[];
|
|
401
|
+
/** Spec 148 T067: an offer deal's agreed publication time. Absent on a pot (its entry carries it) and on older backends. */
|
|
402
|
+
publication?: I_OfferPublication | null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Spec 148 T067: an OFFER deal publishes through 175 US9's auto-publish path.
|
|
407
|
+
* The organization sets the agreed time on a confirmed offer with an approved
|
|
408
|
+
* material (`PATCH /agency/campaigns/:id/members/:memberId/publish-at`); the
|
|
409
|
+
* creator confirms that exact time once (`POST /agency/campaign-invitations/
|
|
410
|
+
* :memberId/publish-at/confirm`, the same route as a pot's); the post then goes
|
|
411
|
+
* out from the creator's own account, and publication is known by construction.
|
|
412
|
+
* `POST .../members/:memberId/published` stays for off-platform posting.
|
|
413
|
+
*/
|
|
414
|
+
export interface I_OfferPublication {
|
|
415
|
+
publishAt: string | null;
|
|
416
|
+
publishConfirmedAt: string | null;
|
|
417
|
+
/** The post is scheduled on the creator's account (the confirmed time is within the signed-link horizon). */
|
|
418
|
+
scheduled: boolean;
|
|
419
|
+
publishedAt: string | null;
|
|
420
|
+
/** The latest refusal at scheduling since the time was set or confirmed; the creator confirms again. */
|
|
421
|
+
publishRefusal: { at: string; reason: string } | null;
|
|
422
|
+
/** Organization side: the offer is confirmed, the latest material approved, and nothing is published yet. */
|
|
423
|
+
canSetPublishAt: boolean;
|
|
424
|
+
/** Creator side: a time is set and not confirmed, and the latest material's caption passes the disclosure check. */
|
|
425
|
+
canConfirmPublishAt: boolean;
|
|
358
426
|
}
|
|
359
427
|
|
|
360
428
|
/** `GET /agency/campaigns/attention` — what waits on the agency (FR-022, the rail). */
|
|
@@ -416,6 +484,10 @@ export interface I_CreatorCampaignView {
|
|
|
416
484
|
canLeave: boolean;
|
|
417
485
|
milestones: I_AgencyMilestone[];
|
|
418
486
|
invitedAt: string;
|
|
487
|
+
/** Spec 148 T067: an offer deal's agreed publication time; null on a pot (see `pot.entry`) and before a confirmed offer. */
|
|
488
|
+
publication?: I_OfferPublication | null;
|
|
489
|
+
/** Spec 148 US8 AC3: what a manager did in this deal for the creator, oldest first. Absent on older backends. */
|
|
490
|
+
delegateActs?: I_CreatorDelegateAct[];
|
|
419
491
|
}
|
|
420
492
|
|
|
421
493
|
/** `GET /agency/campaign-invitations/mine` — pending invitations and live campaigns. */
|
|
@@ -484,8 +556,42 @@ class AgencyCampaignBriefFields extends PotFields {
|
|
|
484
556
|
@Max(20)
|
|
485
557
|
revisionRounds?: number | null;
|
|
486
558
|
|
|
559
|
+
// ── Spec 148: the open listing. Refused by the backend while DEALROOM_ENABLED is off. ──
|
|
560
|
+
|
|
561
|
+
/** `open` lists the campaign in the Dealroom feed; it needs a target profile and the organization's logo. */
|
|
562
|
+
@IsOptional()
|
|
563
|
+
@IsIn(CAMPAIGN_VISIBILITIES as unknown as string[])
|
|
564
|
+
visibility?: CampaignVisibility;
|
|
565
|
+
|
|
566
|
+
/** Checked field by field in the service (flat bodies, see above). Blank constraint = no filter. */
|
|
567
|
+
@IsOptional()
|
|
568
|
+
@IsObject()
|
|
569
|
+
targetProfile?: I_TargetProfile | null;
|
|
570
|
+
|
|
571
|
+
/** Null = unlimited. Once frozen, it may only grow. */
|
|
572
|
+
@IsOptional()
|
|
573
|
+
@IsInt()
|
|
574
|
+
@Min(1)
|
|
575
|
+
@Max(10000)
|
|
576
|
+
slots?: number | null;
|
|
577
|
+
|
|
578
|
+
/** Offer kind only (a pot keeps its own). Once frozen, it may only move later. */
|
|
579
|
+
@IsOptional()
|
|
580
|
+
@IsDateString()
|
|
581
|
+
applicationDeadline?: string | null;
|
|
582
|
+
|
|
583
|
+
/** Any date closes the listing NOW (server time): new applications stop, running deals do not. */
|
|
584
|
+
@IsOptional()
|
|
585
|
+
@IsDateString()
|
|
586
|
+
closedAt?: string | null;
|
|
587
|
+
|
|
487
588
|
protected assignBrief(data?: Partial<AgencyCampaignBriefFields>): void {
|
|
488
589
|
this.assignPot(data);
|
|
590
|
+
this.visibility = data?.visibility;
|
|
591
|
+
this.targetProfile = data?.targetProfile;
|
|
592
|
+
this.slots = data?.slots;
|
|
593
|
+
this.applicationDeadline = data?.applicationDeadline;
|
|
594
|
+
this.closedAt = data?.closedAt;
|
|
489
595
|
this.text = data?.text;
|
|
490
596
|
this.deliverables = data?.deliverables;
|
|
491
597
|
this.draftDueAt = data?.draftDueAt;
|
|
@@ -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
|
+
});
|