ad2app-lib 1.47.0 → 1.49.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 +44 -0
- package/dist/analytics/index.js +17 -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 +391 -0
- package/dist/types/agency-campaigns.js +426 -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 +2 -0
- package/dist/types/index.js +2 -0
- package/package.json +1 -1
- package/src/analytics/index.test.ts +41 -1
- package/src/analytics/index.ts +39 -0
- package/src/types/I_Offer.ts +2 -0
- package/src/types/agency-campaigns.test.ts +85 -0
- package/src/types/agency-campaigns.ts +663 -0
- package/src/types/agency-threads.ts +103 -0
- package/src/types/agency.ts +2 -0
- package/src/types/index.ts +2 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayMaxSize,
|
|
3
|
+
IsArray,
|
|
4
|
+
IsBoolean,
|
|
5
|
+
IsDateString,
|
|
6
|
+
IsIn,
|
|
7
|
+
IsInt,
|
|
8
|
+
IsNotEmpty,
|
|
9
|
+
IsOptional,
|
|
10
|
+
IsString,
|
|
11
|
+
Matches,
|
|
12
|
+
Max,
|
|
13
|
+
MaxLength,
|
|
14
|
+
Min,
|
|
15
|
+
} from 'class-validator';
|
|
16
|
+
|
|
17
|
+
import { I_MediaStage } from './I_Media';
|
|
18
|
+
import { I_OfferStatus } from './I_Offer';
|
|
19
|
+
import { AGENCY_GRANT_SCOPES, type I_AgencyGrantScope } from './agency';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The campaign loop (spec 175 US7) — an organization runs a campaign with the
|
|
23
|
+
* creators on its roster or in the directory.
|
|
24
|
+
*
|
|
25
|
+
* Two steps, never one (Jan, 2026-09-28):
|
|
26
|
+
*
|
|
27
|
+
* 1. MEMBERSHIP — the agency invites a creator; the creator joins or
|
|
28
|
+
* declines. Joining answers any access request folded into the
|
|
29
|
+
* invitation. No money is named here.
|
|
30
|
+
* 2. OFFER — once joined, the agency sends a rate. It is countered, accepted,
|
|
31
|
+
* confirmed or rejected; a creator who leaves withdraws it.
|
|
32
|
+
*
|
|
33
|
+
* The server owns every transition. Each view carries `availableActions`, and
|
|
34
|
+
* a screen renders exactly those acts — it never derives them from a status,
|
|
35
|
+
* so the two can never disagree.
|
|
36
|
+
*
|
|
37
|
+
* See specs/speckit/175-agency-roster/contracts/agency-api.md → US7.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// ── The ONE placements list (FR-025) ────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What a deliverable may be, per platform. A named list in one place, so a
|
|
44
|
+
* "placement" can never be free text that drifts per campaign.
|
|
45
|
+
*/
|
|
46
|
+
export const CAMPAIGN_PLACEMENTS = {
|
|
47
|
+
instagram: ['reel', 'post', 'carousel', 'stories', 'collab_post'],
|
|
48
|
+
tiktok: ['video', 'repost', 'duet', 'stories'],
|
|
49
|
+
youtube: ['video', 'short', 'integration', 'community_post'],
|
|
50
|
+
facebook: ['post', 'reel', 'stories'],
|
|
51
|
+
linkedin: ['post', 'article'],
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
export type CampaignPlatform = keyof typeof CAMPAIGN_PLACEMENTS;
|
|
55
|
+
export type CampaignPlacement = (typeof CAMPAIGN_PLACEMENTS)[CampaignPlatform][number];
|
|
56
|
+
|
|
57
|
+
export const CAMPAIGN_PLATFORMS = Object.keys(CAMPAIGN_PLACEMENTS) as CampaignPlatform[];
|
|
58
|
+
|
|
59
|
+
export const isCampaignPlacement = (platform: string, placement: string): boolean =>
|
|
60
|
+
Object.prototype.hasOwnProperty.call(CAMPAIGN_PLACEMENTS, platform) &&
|
|
61
|
+
(CAMPAIGN_PLACEMENTS[platform as CampaignPlatform] as readonly string[]).includes(placement);
|
|
62
|
+
|
|
63
|
+
// ── The settlement form (FR-027) ─────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/** The creator's own setting. The agency reads it and can never set it. */
|
|
66
|
+
export const SETTLEMENT_FORMS = ['invoice', 'contract', 'barter'] as const;
|
|
67
|
+
export type SettlementForm = (typeof SETTLEMENT_FORMS)[number];
|
|
68
|
+
|
|
69
|
+
// ── States ───────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
export const AGENCY_CAMPAIGN_MEMBER_STATES = ['invited', 'joined', 'declined', 'left', 'removed'] as const;
|
|
72
|
+
export type AgencyCampaignMemberState = (typeof AGENCY_CAMPAIGN_MEMBER_STATES)[number];
|
|
73
|
+
|
|
74
|
+
/** The offer statuses the agency loop writes. `draft`, `deleted` and `alternativeSuggested` are legacy-only. */
|
|
75
|
+
export const AGENCY_OFFER_STATUSES = [
|
|
76
|
+
I_OfferStatus.SENT,
|
|
77
|
+
I_OfferStatus.INFLUENCER_NEGOTIATED,
|
|
78
|
+
I_OfferStatus.AGENCY_NEGOTIATED,
|
|
79
|
+
I_OfferStatus.ACCEPTED,
|
|
80
|
+
I_OfferStatus.CONFIRMED,
|
|
81
|
+
I_OfferStatus.REJECTED,
|
|
82
|
+
I_OfferStatus.WITHDRAWN,
|
|
83
|
+
] as const;
|
|
84
|
+
export type AgencyOfferStatus = (typeof AGENCY_OFFER_STATUSES)[number];
|
|
85
|
+
|
|
86
|
+
export type CampaignSide = 'agency' | 'creator';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Every act on an offer, by either side. Which of them a given caller may take
|
|
90
|
+
* right now is the server's `availableActions`, never a client guess.
|
|
91
|
+
*
|
|
92
|
+
* agency: counter · confirm · reject
|
|
93
|
+
* creator: accept · counter · reject
|
|
94
|
+
*/
|
|
95
|
+
export const AGENCY_OFFER_ACTIONS = ['accept', 'counter', 'confirm', 'reject'] as const;
|
|
96
|
+
export type AgencyOfferAction = (typeof AGENCY_OFFER_ACTIONS)[number];
|
|
97
|
+
|
|
98
|
+
/** The agency's acts on the latest material; the creator's only act is to upload. */
|
|
99
|
+
export const AGENCY_MATERIAL_ACTIONS = ['approve', 'request_changes', 'reject'] as const;
|
|
100
|
+
export type AgencyMaterialAction = (typeof AGENCY_MATERIAL_ACTIONS)[number];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The board's columns, in order (FR-024). A creator stands in exactly one.
|
|
104
|
+
* `declined`, `left`, `removed` and `rejected` are OFF the board: those rows
|
|
105
|
+
* stay in the list with their date (US7 scenario 9), never in a column.
|
|
106
|
+
*/
|
|
107
|
+
export const AGENCY_CAMPAIGN_STEPS = [
|
|
108
|
+
'invited',
|
|
109
|
+
'joined',
|
|
110
|
+
'offer_sent',
|
|
111
|
+
'negotiating',
|
|
112
|
+
'confirmed',
|
|
113
|
+
'material_in',
|
|
114
|
+
'approved',
|
|
115
|
+
'published',
|
|
116
|
+
] as const;
|
|
117
|
+
export type AgencyCampaignStep = (typeof AGENCY_CAMPAIGN_STEPS)[number];
|
|
118
|
+
|
|
119
|
+
export type AgencyCampaignOffBoard = 'declined' | 'left' | 'removed' | 'rejected';
|
|
120
|
+
|
|
121
|
+
export const AGENCY_MILESTONE_KINDS = ['draft_due', 'live_by', 'custom'] as const;
|
|
122
|
+
export type AgencyMilestoneKind = (typeof AGENCY_MILESTONE_KINDS)[number];
|
|
123
|
+
|
|
124
|
+
/** A milestone may wait on one rule: every ACTIVE offer at a status, or every active creator's latest material at a stage. */
|
|
125
|
+
export const AGENCY_MILESTONE_OFFER_RULES = [I_OfferStatus.CONFIRMED] as const;
|
|
126
|
+
export const AGENCY_MILESTONE_MEDIA_RULES = [I_MediaStage.UPLOADED, I_MediaStage.ACCEPTED] as const;
|
|
127
|
+
|
|
128
|
+
export const AGENCY_CAMPAIGN_EVENT_KINDS = [
|
|
129
|
+
'invited',
|
|
130
|
+
'joined',
|
|
131
|
+
'declined',
|
|
132
|
+
'left',
|
|
133
|
+
'removed',
|
|
134
|
+
'offer_sent',
|
|
135
|
+
'offer_countered',
|
|
136
|
+
'offer_accepted',
|
|
137
|
+
'offer_confirmed',
|
|
138
|
+
'offer_rejected',
|
|
139
|
+
'offer_withdrawn',
|
|
140
|
+
'material_uploaded',
|
|
141
|
+
'material_approved',
|
|
142
|
+
'material_changes_asked',
|
|
143
|
+
'material_rejected',
|
|
144
|
+
'published',
|
|
145
|
+
'milestone_confirmed',
|
|
146
|
+
] as const;
|
|
147
|
+
export type AgencyCampaignEventKind = (typeof AGENCY_CAMPAIGN_EVENT_KINDS)[number];
|
|
148
|
+
|
|
149
|
+
/** Refusal codes the frontend owes its own copy for. */
|
|
150
|
+
export const AGENCY_CAMPAIGN_CODES = {
|
|
151
|
+
/** The transition is not open at this state, or not for this side. */
|
|
152
|
+
TRANSITION_REFUSED: 'agency-campaign-transition-refused',
|
|
153
|
+
/** A campaign sent to a creator must carry its brief text (FR-025). */
|
|
154
|
+
BRIEF_REQUIRED: 'agency-campaign-brief-required',
|
|
155
|
+
/** A deliverable on a platform the creator has not connected (FR-025). */
|
|
156
|
+
PLATFORM_NOT_CONNECTED: 'agency-campaign-platform-not-connected',
|
|
157
|
+
/** The creator is already on this campaign. */
|
|
158
|
+
ALREADY_MEMBER: 'agency-campaign-already-member',
|
|
159
|
+
/** The upload was confirmed but no stored file exists at its path (FR-029). */
|
|
160
|
+
MATERIAL_NOT_STORED: 'agency-campaign-material-not-stored',
|
|
161
|
+
/** A demo creator cannot be invited or mailed (spec 179). */
|
|
162
|
+
DEMO_CREATOR: 'agency-campaign-demo-creator',
|
|
163
|
+
} as const;
|
|
164
|
+
export type AgencyCampaignCode = (typeof AGENCY_CAMPAIGN_CODES)[keyof typeof AGENCY_CAMPAIGN_CODES];
|
|
165
|
+
|
|
166
|
+
// ── Views ────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export interface I_CampaignDeliverable {
|
|
169
|
+
platform: CampaignPlatform;
|
|
170
|
+
placement: CampaignPlacement;
|
|
171
|
+
qty: number;
|
|
172
|
+
note?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface I_AgencyCampaignBrief {
|
|
176
|
+
/** The concept, in prose. May be empty only while the campaign is a draft. */
|
|
177
|
+
text: string;
|
|
178
|
+
deliverables: I_CampaignDeliverable[];
|
|
179
|
+
/** ISO dates, or null when not set. */
|
|
180
|
+
draftDueAt: string | null;
|
|
181
|
+
liveByAt: string | null;
|
|
182
|
+
/** Usage rights on the brand's profiles, in months. */
|
|
183
|
+
licenceMonths: number | null;
|
|
184
|
+
/** Paid promotion (whitelisting), in months. */
|
|
185
|
+
paidPromoMonths: number | null;
|
|
186
|
+
revisionRounds: number | null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Money is integer minor units plus an ISO-4217 code; the legacy float `price` is never written. */
|
|
190
|
+
export interface I_Money {
|
|
191
|
+
minor: number;
|
|
192
|
+
currency: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface I_AgencyOfferView {
|
|
196
|
+
id: string;
|
|
197
|
+
status: AgencyOfferStatus;
|
|
198
|
+
/** The price on the table now: the latest one either side named. */
|
|
199
|
+
price: I_Money;
|
|
200
|
+
/** Whose move it is; null once the offer is settled. */
|
|
201
|
+
turn: CampaignSide | null;
|
|
202
|
+
/** The acts open to the CALLER'S side, right now. */
|
|
203
|
+
availableActions: AgencyOfferAction[];
|
|
204
|
+
sentAt: string;
|
|
205
|
+
updatedAt: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface I_AgencyMaterial {
|
|
209
|
+
id: string;
|
|
210
|
+
fileName: string;
|
|
211
|
+
contentType: string;
|
|
212
|
+
sizeBytes: number | null;
|
|
213
|
+
stage: I_MediaStage;
|
|
214
|
+
uploadedAt: string;
|
|
215
|
+
/** The note sent with a change request or a rejection of THIS version. */
|
|
216
|
+
note: string | null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface I_AgencyMilestone {
|
|
220
|
+
id: string;
|
|
221
|
+
kind: AgencyMilestoneKind;
|
|
222
|
+
/** Only for `custom`; the others are named by i18n from `kind`. */
|
|
223
|
+
label: string | null;
|
|
224
|
+
dueAt: string | null;
|
|
225
|
+
/** Null = campaign-wide. */
|
|
226
|
+
creatorUserId: string | null;
|
|
227
|
+
offerRule: (typeof AGENCY_MILESTONE_OFFER_RULES)[number] | null;
|
|
228
|
+
mediaRule: (typeof AGENCY_MILESTONE_MEDIA_RULES)[number] | null;
|
|
229
|
+
readyToConfirm: boolean;
|
|
230
|
+
confirmedAt: string | null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface I_AgencyCampaignEvent {
|
|
234
|
+
kind: AgencyCampaignEventKind;
|
|
235
|
+
side: CampaignSide;
|
|
236
|
+
at: string;
|
|
237
|
+
price?: I_Money;
|
|
238
|
+
note?: string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface I_CampaignCreatorIdentity {
|
|
242
|
+
userId: string | null;
|
|
243
|
+
handle: string;
|
|
244
|
+
displayName?: string;
|
|
245
|
+
avatarUrl?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** One creator's row on the campaign screen (the list twin of the board). */
|
|
249
|
+
export interface I_AgencyCampaignMemberRow {
|
|
250
|
+
memberId: string;
|
|
251
|
+
creator: I_CampaignCreatorIdentity;
|
|
252
|
+
state: AgencyCampaignMemberState;
|
|
253
|
+
/** The board column, or the reason the row is off the board. */
|
|
254
|
+
step: AgencyCampaignStep | AgencyCampaignOffBoard;
|
|
255
|
+
/** When the row reached its current step. */
|
|
256
|
+
stepAt: string;
|
|
257
|
+
offer: I_AgencyOfferView | null;
|
|
258
|
+
latestMaterial: I_AgencyMaterial | null;
|
|
259
|
+
/** The agency's acts on `latestMaterial`. */
|
|
260
|
+
materialActions: AgencyMaterialAction[];
|
|
261
|
+
/** Read from the creator's own setting (FR-027); null when they have not set it. */
|
|
262
|
+
settlementForm: SettlementForm | null;
|
|
263
|
+
nextDeadline: { at: string; kind: AgencyMilestoneKind; label: string | null } | null;
|
|
264
|
+
/** True when the agency may still remove them (no confirmed offer). */
|
|
265
|
+
removable: boolean;
|
|
266
|
+
/** True when the agency may send an offer now (joined, none sent yet). */
|
|
267
|
+
canSendOffer: boolean;
|
|
268
|
+
canMarkPublished: boolean;
|
|
269
|
+
/** Scopes a folded access request asked for, while the invitation is open. */
|
|
270
|
+
requestedScopes: I_AgencyGrantScope[];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export interface I_AgencyCampaignPipeline {
|
|
274
|
+
/** Creators per board column; off-board rows are not counted. */
|
|
275
|
+
steps: Record<AgencyCampaignStep, number>;
|
|
276
|
+
total: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface I_AgencyCampaignCard {
|
|
280
|
+
id: string;
|
|
281
|
+
name: string;
|
|
282
|
+
isDraft: boolean;
|
|
283
|
+
liveByAt: string | null;
|
|
284
|
+
draftDueAt: string | null;
|
|
285
|
+
pipeline: I_AgencyCampaignPipeline;
|
|
286
|
+
/** How many things on this campaign wait on the agency (the rail's definition). */
|
|
287
|
+
attention: number;
|
|
288
|
+
createdAt: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface I_AgencyCampaign {
|
|
292
|
+
id: string;
|
|
293
|
+
name: string;
|
|
294
|
+
isDraft: boolean;
|
|
295
|
+
brief: I_AgencyCampaignBrief;
|
|
296
|
+
members: I_AgencyCampaignMemberRow[];
|
|
297
|
+
milestones: I_AgencyMilestone[];
|
|
298
|
+
pipeline: I_AgencyCampaignPipeline;
|
|
299
|
+
createdAt: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Opening one creator (FR-024): their course with dates, notes, files and milestones. */
|
|
303
|
+
export interface I_AgencyCampaignCreatorDetail {
|
|
304
|
+
row: I_AgencyCampaignMemberRow;
|
|
305
|
+
creatorBrief: string | null;
|
|
306
|
+
events: I_AgencyCampaignEvent[];
|
|
307
|
+
materials: I_AgencyMaterial[];
|
|
308
|
+
milestones: I_AgencyMilestone[];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** `GET /agency/campaigns/attention` — what waits on the agency (FR-022, the rail). */
|
|
312
|
+
export interface I_AgencyCampaignAttention {
|
|
313
|
+
counterOffers: number;
|
|
314
|
+
materialsToReview: number;
|
|
315
|
+
milestonesReady: number;
|
|
316
|
+
total: number;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** `GET /agency/panel` (FR-022). */
|
|
320
|
+
export interface I_AgencyPanel {
|
|
321
|
+
/** Open milestones of running (non-draft) campaigns due today. */
|
|
322
|
+
dueToday: number;
|
|
323
|
+
/** Open milestones of running (non-draft) campaigns past their date. */
|
|
324
|
+
overdue: number;
|
|
325
|
+
/** The roster's own "needs attention" count, or null when it could not be read: shown as a dash, never 0. */
|
|
326
|
+
needsAttention: number | null;
|
|
327
|
+
deadlines: Array<{
|
|
328
|
+
campaignId: string;
|
|
329
|
+
campaignName: string;
|
|
330
|
+
milestoneId: string;
|
|
331
|
+
kind: AgencyMilestoneKind;
|
|
332
|
+
label: string | null;
|
|
333
|
+
dueAt: string;
|
|
334
|
+
creatorHandle: string | null;
|
|
335
|
+
confirmed: boolean;
|
|
336
|
+
}>;
|
|
337
|
+
campaigns: Array<{ id: string; name: string; pipeline: I_AgencyCampaignPipeline; liveByAt: string | null }>;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── The creator's side ───────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* What a creator reads about ONE campaign they are on. Deliberately narrow
|
|
344
|
+
* (SC-014): their own membership, offer, brief, materials and deadlines — no
|
|
345
|
+
* other creator, no total, no average, no count of others.
|
|
346
|
+
*/
|
|
347
|
+
export interface I_CreatorCampaignView {
|
|
348
|
+
memberId: string;
|
|
349
|
+
state: AgencyCampaignMemberState;
|
|
350
|
+
organization: { id: string; name: string };
|
|
351
|
+
campaign: { id: string; name: string; brief: I_AgencyCampaignBrief };
|
|
352
|
+
creatorBrief: string | null;
|
|
353
|
+
/** The access request folded into the invitation, while it is open. */
|
|
354
|
+
requestedScopes: I_AgencyGrantScope[];
|
|
355
|
+
offer: I_AgencyOfferView | null;
|
|
356
|
+
materials: I_AgencyMaterial[];
|
|
357
|
+
/** True when the creator may upload now (confirmed, and no version awaits review). */
|
|
358
|
+
canUpload: boolean;
|
|
359
|
+
/** True when the creator may leave (no confirmed offer). */
|
|
360
|
+
canLeave: boolean;
|
|
361
|
+
milestones: I_AgencyMilestone[];
|
|
362
|
+
invitedAt: string;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** `GET /agency/campaign-invitations/mine` — pending invitations and live campaigns. */
|
|
366
|
+
export interface I_CreatorCampaigns {
|
|
367
|
+
items: I_CreatorCampaignView[];
|
|
368
|
+
/** The creator's turn, the creator's badge: invitations to answer, offers to answer, and materials to upload (a first version, or a new one after changes were asked or a version was rejected). */
|
|
369
|
+
waitingOnYou: number;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface I_MaterialUploadTicket {
|
|
373
|
+
/** Opaque; sent back on confirm. */
|
|
374
|
+
uploadId: string;
|
|
375
|
+
/** PUT the file's bytes here with `headers`. */
|
|
376
|
+
uploadUrl: string;
|
|
377
|
+
headers: Record<string, string>;
|
|
378
|
+
expiresAt: string;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export interface I_SignedRead {
|
|
382
|
+
url: string;
|
|
383
|
+
expiresAt: string;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── Bodies ───────────────────────────────────────────────────────────────────
|
|
387
|
+
//
|
|
388
|
+
// Flat on purpose: nested bodies need class-transformer's `@Type`, and a
|
|
389
|
+
// linked lib carries its own copy whose metadata the backend's ValidationPipe
|
|
390
|
+
// never reads. `deliverables` is an array of plain objects checked item by
|
|
391
|
+
// item in the service against CAMPAIGN_PLACEMENTS — the pair is the rule.
|
|
392
|
+
|
|
393
|
+
/** The brief's fields, shared by create and update. All optional on the wire. */
|
|
394
|
+
class AgencyCampaignBriefFields {
|
|
395
|
+
@IsOptional()
|
|
396
|
+
@IsString()
|
|
397
|
+
@MaxLength(10000)
|
|
398
|
+
text?: string;
|
|
399
|
+
|
|
400
|
+
@IsOptional()
|
|
401
|
+
@IsArray()
|
|
402
|
+
@ArrayMaxSize(30)
|
|
403
|
+
deliverables?: I_CampaignDeliverable[];
|
|
404
|
+
|
|
405
|
+
@IsOptional()
|
|
406
|
+
@IsDateString()
|
|
407
|
+
draftDueAt?: string | null;
|
|
408
|
+
|
|
409
|
+
@IsOptional()
|
|
410
|
+
@IsDateString()
|
|
411
|
+
liveByAt?: string | null;
|
|
412
|
+
|
|
413
|
+
@IsOptional()
|
|
414
|
+
@IsInt()
|
|
415
|
+
@Min(0)
|
|
416
|
+
@Max(120)
|
|
417
|
+
licenceMonths?: number | null;
|
|
418
|
+
|
|
419
|
+
@IsOptional()
|
|
420
|
+
@IsInt()
|
|
421
|
+
@Min(0)
|
|
422
|
+
@Max(120)
|
|
423
|
+
paidPromoMonths?: number | null;
|
|
424
|
+
|
|
425
|
+
@IsOptional()
|
|
426
|
+
@IsInt()
|
|
427
|
+
@Min(0)
|
|
428
|
+
@Max(20)
|
|
429
|
+
revisionRounds?: number | null;
|
|
430
|
+
|
|
431
|
+
protected assignBrief(data?: Partial<AgencyCampaignBriefFields>): void {
|
|
432
|
+
this.text = data?.text;
|
|
433
|
+
this.deliverables = data?.deliverables;
|
|
434
|
+
this.draftDueAt = data?.draftDueAt;
|
|
435
|
+
this.liveByAt = data?.liveByAt;
|
|
436
|
+
this.licenceMonths = data?.licenceMonths;
|
|
437
|
+
this.paidPromoMonths = data?.paidPromoMonths;
|
|
438
|
+
this.revisionRounds = data?.revisionRounds;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export class CreateAgencyCampaignDto extends AgencyCampaignBriefFields {
|
|
443
|
+
@IsString()
|
|
444
|
+
@IsNotEmpty()
|
|
445
|
+
@MaxLength(128)
|
|
446
|
+
name: string;
|
|
447
|
+
|
|
448
|
+
constructor(data?: Partial<CreateAgencyCampaignDto>) {
|
|
449
|
+
super();
|
|
450
|
+
this.name = data?.name ?? '';
|
|
451
|
+
this.assignBrief(data);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export class UpdateAgencyCampaignDto extends AgencyCampaignBriefFields {
|
|
456
|
+
@IsOptional()
|
|
457
|
+
@IsString()
|
|
458
|
+
@IsNotEmpty()
|
|
459
|
+
@MaxLength(128)
|
|
460
|
+
name?: string;
|
|
461
|
+
|
|
462
|
+
/** Setting false publishes the draft; the brief text must then be present (FR-025). */
|
|
463
|
+
@IsOptional()
|
|
464
|
+
@IsBoolean()
|
|
465
|
+
isDraft?: boolean;
|
|
466
|
+
|
|
467
|
+
constructor(data?: Partial<UpdateAgencyCampaignDto>) {
|
|
468
|
+
super();
|
|
469
|
+
this.name = data?.name;
|
|
470
|
+
this.isDraft = data?.isDraft;
|
|
471
|
+
this.assignBrief(data);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export class InviteCampaignCreatorDto {
|
|
476
|
+
/** A user id: a UUID or a Firebase uid. */
|
|
477
|
+
@IsString()
|
|
478
|
+
@Matches(/^[A-Za-z0-9-]{1,128}$/)
|
|
479
|
+
creatorUserId: string;
|
|
480
|
+
|
|
481
|
+
@IsOptional()
|
|
482
|
+
@IsString()
|
|
483
|
+
@MaxLength(5000)
|
|
484
|
+
creatorBrief?: string;
|
|
485
|
+
|
|
486
|
+
/** The access request folded into the invitation when no grant covers them (FR-026). */
|
|
487
|
+
@IsOptional()
|
|
488
|
+
@IsArray()
|
|
489
|
+
@IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
|
|
490
|
+
scopes?: I_AgencyGrantScope[];
|
|
491
|
+
|
|
492
|
+
constructor(data?: Partial<InviteCampaignCreatorDto>) {
|
|
493
|
+
this.creatorUserId = data?.creatorUserId ?? '';
|
|
494
|
+
this.creatorBrief = data?.creatorBrief;
|
|
495
|
+
this.scopes = data?.scopes;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export class SendCampaignOfferDto {
|
|
500
|
+
@IsInt()
|
|
501
|
+
@Min(0)
|
|
502
|
+
@Max(1_000_000_000)
|
|
503
|
+
priceMinor: number;
|
|
504
|
+
|
|
505
|
+
@IsString()
|
|
506
|
+
@Matches(/^[A-Z]{3}$/)
|
|
507
|
+
currency: string;
|
|
508
|
+
|
|
509
|
+
constructor(data?: Partial<SendCampaignOfferDto>) {
|
|
510
|
+
this.priceMinor = data?.priceMinor ?? 0;
|
|
511
|
+
this.currency = data?.currency ?? 'PLN';
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** One act on an offer. `counter` needs `priceMinor`; any act may carry a note. */
|
|
516
|
+
export class CampaignOfferActDto {
|
|
517
|
+
@IsIn(AGENCY_OFFER_ACTIONS as unknown as string[])
|
|
518
|
+
action: AgencyOfferAction;
|
|
519
|
+
|
|
520
|
+
/** A counter keeps the offer's currency; only the amount moves. */
|
|
521
|
+
@IsOptional()
|
|
522
|
+
@IsInt()
|
|
523
|
+
@Min(0)
|
|
524
|
+
@Max(1_000_000_000)
|
|
525
|
+
priceMinor?: number;
|
|
526
|
+
|
|
527
|
+
@IsOptional()
|
|
528
|
+
@IsString()
|
|
529
|
+
@MaxLength(2000)
|
|
530
|
+
note?: string;
|
|
531
|
+
|
|
532
|
+
constructor(data?: Partial<CampaignOfferActDto>) {
|
|
533
|
+
this.action = data?.action ?? 'accept';
|
|
534
|
+
this.priceMinor = data?.priceMinor;
|
|
535
|
+
this.note = data?.note;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** One agency act on a material. `request_changes` requires the note. */
|
|
540
|
+
export class CampaignMaterialActDto {
|
|
541
|
+
@IsIn(AGENCY_MATERIAL_ACTIONS as unknown as string[])
|
|
542
|
+
action: AgencyMaterialAction;
|
|
543
|
+
|
|
544
|
+
@IsOptional()
|
|
545
|
+
@IsString()
|
|
546
|
+
@MaxLength(2000)
|
|
547
|
+
note?: string;
|
|
548
|
+
|
|
549
|
+
constructor(data?: Partial<CampaignMaterialActDto>) {
|
|
550
|
+
this.action = data?.action ?? 'approve';
|
|
551
|
+
this.note = data?.note;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export class JoinCampaignDto {
|
|
556
|
+
/** A subset of what was asked; absent = everything asked. */
|
|
557
|
+
@IsOptional()
|
|
558
|
+
@IsArray()
|
|
559
|
+
@IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
|
|
560
|
+
scopes?: I_AgencyGrantScope[];
|
|
561
|
+
|
|
562
|
+
constructor(data?: Partial<JoinCampaignDto>) {
|
|
563
|
+
this.scopes = data?.scopes;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Material files: images and video only, at most 2 GB. */
|
|
568
|
+
export const MATERIAL_MAX_BYTES = 2 * 1024 * 1024 * 1024;
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* The photo formats a material may be. Raster only: no SVG or HTML, because a
|
|
572
|
+
* material is opened in the browser. Shared so the upload picker, the
|
|
573
|
+
* creator's own check and the API refuse exactly the same files.
|
|
574
|
+
*/
|
|
575
|
+
export const MATERIAL_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/heic', 'image/heif', 'image/avif'] as const;
|
|
576
|
+
|
|
577
|
+
/** A material's content type: one of MATERIAL_IMAGE_TYPES, or any video. */
|
|
578
|
+
export const MATERIAL_CONTENT_TYPE = new RegExp(
|
|
579
|
+
`^(${MATERIAL_IMAGE_TYPES.map((type) => type.replace(/[/+.]/g, (c) => `\\${c}`)).join('|')}|video\\/[A-Za-z0-9.+-]+)$`,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
export class PrepareMaterialUploadDto {
|
|
583
|
+
@IsString()
|
|
584
|
+
@IsNotEmpty()
|
|
585
|
+
@MaxLength(255)
|
|
586
|
+
fileName: string;
|
|
587
|
+
|
|
588
|
+
@IsString()
|
|
589
|
+
/** Raster images and video (MATERIAL_CONTENT_TYPE). */
|
|
590
|
+
@Matches(MATERIAL_CONTENT_TYPE)
|
|
591
|
+
contentType: string;
|
|
592
|
+
|
|
593
|
+
@IsInt()
|
|
594
|
+
@Min(1)
|
|
595
|
+
@Max(MATERIAL_MAX_BYTES)
|
|
596
|
+
sizeBytes: number;
|
|
597
|
+
|
|
598
|
+
constructor(data?: Partial<PrepareMaterialUploadDto>) {
|
|
599
|
+
this.fileName = data?.fileName ?? '';
|
|
600
|
+
this.contentType = data?.contentType ?? '';
|
|
601
|
+
this.sizeBytes = data?.sizeBytes ?? 0;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export class ConfirmMaterialUploadDto {
|
|
606
|
+
@IsString()
|
|
607
|
+
@IsNotEmpty()
|
|
608
|
+
@MaxLength(512)
|
|
609
|
+
uploadId: string;
|
|
610
|
+
|
|
611
|
+
constructor(data?: Partial<ConfirmMaterialUploadDto>) {
|
|
612
|
+
this.uploadId = data?.uploadId ?? '';
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export class CreateAgencyMilestoneDto {
|
|
617
|
+
@IsIn(AGENCY_MILESTONE_KINDS as unknown as string[])
|
|
618
|
+
kind: AgencyMilestoneKind;
|
|
619
|
+
|
|
620
|
+
/** Required for `custom`, ignored otherwise. */
|
|
621
|
+
@IsOptional()
|
|
622
|
+
@IsString()
|
|
623
|
+
@MaxLength(256)
|
|
624
|
+
label?: string;
|
|
625
|
+
|
|
626
|
+
@IsOptional()
|
|
627
|
+
@IsDateString()
|
|
628
|
+
dueAt?: string | null;
|
|
629
|
+
|
|
630
|
+
/** Absent = campaign-wide. */
|
|
631
|
+
@IsOptional()
|
|
632
|
+
@IsString()
|
|
633
|
+
@Matches(/^[A-Za-z0-9-]{1,128}$/)
|
|
634
|
+
creatorUserId?: string | null;
|
|
635
|
+
|
|
636
|
+
@IsOptional()
|
|
637
|
+
@IsIn(AGENCY_MILESTONE_OFFER_RULES as unknown as string[])
|
|
638
|
+
offerRule?: (typeof AGENCY_MILESTONE_OFFER_RULES)[number] | null;
|
|
639
|
+
|
|
640
|
+
@IsOptional()
|
|
641
|
+
@IsIn(AGENCY_MILESTONE_MEDIA_RULES as unknown as string[])
|
|
642
|
+
mediaRule?: (typeof AGENCY_MILESTONE_MEDIA_RULES)[number] | null;
|
|
643
|
+
|
|
644
|
+
constructor(data?: Partial<CreateAgencyMilestoneDto>) {
|
|
645
|
+
this.kind = data?.kind ?? 'custom';
|
|
646
|
+
this.label = data?.label;
|
|
647
|
+
this.dueAt = data?.dueAt;
|
|
648
|
+
this.creatorUserId = data?.creatorUserId;
|
|
649
|
+
this.offerRule = data?.offerRule;
|
|
650
|
+
this.mediaRule = data?.mediaRule;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/** `PATCH /users/me/settlement-form` — the creator's own setting (FR-027). */
|
|
655
|
+
export class UpdateSettlementFormDto {
|
|
656
|
+
@IsOptional()
|
|
657
|
+
@IsIn(SETTLEMENT_FORMS as unknown as string[])
|
|
658
|
+
settlementForm: SettlementForm | null;
|
|
659
|
+
|
|
660
|
+
constructor(data?: Partial<UpdateSettlementFormDto>) {
|
|
661
|
+
this.settlementForm = data?.settlementForm ?? null;
|
|
662
|
+
}
|
|
663
|
+
}
|