ad2app-lib 1.44.0 → 1.47.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/types/I_AccessDenial.d.ts +19 -0
- package/dist/types/I_AccessDenial.js +19 -0
- package/dist/types/agency.d.ts +277 -17
- package/dist/types/agency.js +182 -33
- package/dist/types/scheduling/I_SchedulingProUpgrade.d.ts +10 -1
- package/dist/types/scheduling/I_SchedulingProUpgrade.js +4 -0
- package/package.json +1 -1
- package/src/types/I_AccessDenial.ts +19 -0
- package/src/types/access-denial.test.ts +1 -1
- package/src/types/agency-billing.test.ts +83 -0
- package/src/types/agency-discover.test.ts +79 -0
- package/src/types/agency.test.ts +79 -1
- package/src/types/agency.ts +363 -34
- package/src/types/scheduling/I_SchedulingProUpgrade.ts +13 -0
package/src/types/agency.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrayNotEmpty,
|
|
3
|
+
IsArray,
|
|
4
|
+
IsBoolean,
|
|
5
|
+
IsEmail,
|
|
6
|
+
IsIn,
|
|
7
|
+
IsNotEmpty,
|
|
8
|
+
IsOptional,
|
|
9
|
+
IsString,
|
|
10
|
+
IsUrl,
|
|
11
|
+
IsUUID,
|
|
12
|
+
Length,
|
|
13
|
+
Matches,
|
|
14
|
+
} from 'class-validator';
|
|
15
|
+
|
|
1
16
|
/**
|
|
2
17
|
* Agency domain — organizations, memberships and creator access grants (spec 175).
|
|
3
18
|
*
|
|
@@ -132,6 +147,12 @@ export interface I_AgencyMember {
|
|
|
132
147
|
/** Null while an e-mail invitation is outstanding — nobody has accepted yet. */
|
|
133
148
|
userId?: string;
|
|
134
149
|
invitedEmail: string;
|
|
150
|
+
/**
|
|
151
|
+
* The account's own e-mail once the invitation is accepted (and always for
|
|
152
|
+
* the owner, who applied rather than being invited). The Team panel names a
|
|
153
|
+
* person by this, never by the organization's contact address (T063).
|
|
154
|
+
*/
|
|
155
|
+
accountEmail?: string;
|
|
135
156
|
role: AgencyMemberRole;
|
|
136
157
|
invitedAt: string;
|
|
137
158
|
/** "Who is in" is exactly `acceptedAt != null`. */
|
|
@@ -203,8 +224,30 @@ export interface I_CreatorAccessGrant {
|
|
|
203
224
|
sponsorship?: { tier: SponsoredTier; since: string; until?: string };
|
|
204
225
|
/** Which platforms the grant reaches right now — derived at read time, never stored. */
|
|
205
226
|
platformsReached: PublishPlatform[];
|
|
227
|
+
/**
|
|
228
|
+
* The agency's OPEN second ask on an active grant, for scopes the creator
|
|
229
|
+
* did not grant (FR-008). Kept apart from `requestedScopes`, so the consent
|
|
230
|
+
* screen names exactly this ask and nothing refused earlier (review H4).
|
|
231
|
+
* Absent when there is no open ask.
|
|
232
|
+
*/
|
|
233
|
+
reaskedScopes?: I_AgencyGrantScope[];
|
|
234
|
+
/** When that open ask expires unanswered. */
|
|
235
|
+
reaskExpiresAt?: string;
|
|
206
236
|
}
|
|
207
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Outcomes of discovery and requests that are not refusals of access, sent as
|
|
240
|
+
* codes so the screen composes the sentence in the reader's language (FR-019,
|
|
241
|
+
* review M4). A 429 carries `{ code, limit, windowMinutes }`.
|
|
242
|
+
*/
|
|
243
|
+
export const AGENCY_REQUEST_CODES = {
|
|
244
|
+
RATE_LIMITED: 'agency-rate-limited',
|
|
245
|
+
REQUEST_EXPIRED: 'agency-request-expired',
|
|
246
|
+
REQUEST_ANSWERED: 'agency-request-answered',
|
|
247
|
+
} as const;
|
|
248
|
+
|
|
249
|
+
export type AgencyRequestCode = (typeof AGENCY_REQUEST_CODES)[keyof typeof AGENCY_REQUEST_CODES];
|
|
250
|
+
|
|
208
251
|
// ── Roster ───────────────────────────────────────────────────────────────────
|
|
209
252
|
|
|
210
253
|
/**
|
|
@@ -221,6 +264,13 @@ export const AGENCY_ROSTER_ROW_STATES = [
|
|
|
221
264
|
'lapsed',
|
|
222
265
|
/** the creator has no live platform connection */
|
|
223
266
|
'disconnected',
|
|
267
|
+
/**
|
|
268
|
+
* shared within the last 24 hours and not synced yet: the first daily sync
|
|
269
|
+
* has not run. Not stale, and not something the agency can act on (T067,
|
|
270
|
+
* the 2026-09-26 UI run: a creator who agreed a minute ago read as
|
|
271
|
+
* "needs attention").
|
|
272
|
+
*/
|
|
273
|
+
'syncing',
|
|
224
274
|
/** no successful daily sync in 24 hours (FR-011, Jan 2026-09-21) */
|
|
225
275
|
'stale',
|
|
226
276
|
'active',
|
|
@@ -244,30 +294,115 @@ export const AGENCY_ROSTER_NEEDS_ATTENTION_STATES = [
|
|
|
244
294
|
export type AgencyRosterNeedsAttentionState =
|
|
245
295
|
(typeof AGENCY_ROSTER_NEEDS_ATTENTION_STATES)[number];
|
|
246
296
|
|
|
297
|
+
/**
|
|
298
|
+
* The rows that carry numbers — ONE home for "which creators' numbers may the
|
|
299
|
+
* agency see on the roster" (decided 2026-09-24, review M1/M2).
|
|
300
|
+
*
|
|
301
|
+
* Only a row whose card OPENS carries `metrics` and counts in the followers,
|
|
302
|
+
* followers-delta and comments totals: `active`, and `stale` (the card opens;
|
|
303
|
+
* the numbers are as old as `lastSyncAt` says). A `lapsed` creator's reads are
|
|
304
|
+
* refused on the card, so their numbers must not reach the agency through the
|
|
305
|
+
* roster instead; a `disconnected` creator has nothing live to count; a
|
|
306
|
+
* `requested` creator has shared nothing yet.
|
|
307
|
+
*/
|
|
308
|
+
export const AGENCY_ROSTER_METRICS_STATES = [
|
|
309
|
+
'syncing',
|
|
310
|
+
'stale',
|
|
311
|
+
'active',
|
|
312
|
+
] as const satisfies readonly AgencyRosterRowState[];
|
|
313
|
+
|
|
314
|
+
export type AgencyRosterMetricsState = (typeof AGENCY_ROSTER_METRICS_STATES)[number];
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* One creator's numbers on the roster (FR-011a: sortable metric columns), read
|
|
318
|
+
* from the daily-synced tables, never a vendor call. `null` is a REASON, never
|
|
319
|
+
* a zero: the creator did not grant that scope, or nothing has been synced yet.
|
|
320
|
+
*/
|
|
321
|
+
export interface I_AgencyRosterRowMetrics {
|
|
322
|
+
/** Latest followers over the creator's CONNECTED platforms; null without `analytics` or before the first sync. */
|
|
323
|
+
followers: number | null;
|
|
324
|
+
/** Against the snapshot seven days earlier, per connected platform; null without `analytics` or without a week-old point. */
|
|
325
|
+
followersDelta7d: number | null;
|
|
326
|
+
/** The current Monday-to-Sunday week, organization timezone; null without `comments`. */
|
|
327
|
+
commentsThisWeek: number | null;
|
|
328
|
+
}
|
|
329
|
+
|
|
247
330
|
export interface I_AgencyRosterRow {
|
|
248
331
|
grantId: string;
|
|
249
332
|
creator: { id: string; handle: string };
|
|
250
333
|
platforms: PublishPlatform[];
|
|
251
334
|
state: AgencyRosterRowState;
|
|
252
335
|
grantedScopes: I_AgencyGrantScope[];
|
|
336
|
+
/**
|
|
337
|
+
* The newest successful daily sync. Absent on a `requested` row (nothing is
|
|
338
|
+
* shared yet, so there is nothing to have synced for the agency) and on a
|
|
339
|
+
* creator never synced. `platforms` stays on a requested row: it is what the
|
|
340
|
+
* consent screen names, a fact about the account rather than shared data.
|
|
341
|
+
*/
|
|
253
342
|
lastSyncAt?: string;
|
|
343
|
+
/**
|
|
344
|
+
* Present ONLY on rows in AGENCY_ROSTER_METRICS_STATES (`active`, `stale`):
|
|
345
|
+
* the rows whose card opens. Absent on `lapsed`, `disconnected` and
|
|
346
|
+
* `requested` rows — no numbers, not null numbers.
|
|
347
|
+
*/
|
|
348
|
+
metrics?: I_AgencyRosterRowMetrics;
|
|
349
|
+
/**
|
|
350
|
+
* The agency's own last "Ask again" (FR-008), while its one-per-TTL clock
|
|
351
|
+
* runs (audit F19): when it was sent and when another is possible. It says
|
|
352
|
+
* nothing about the ANSWER, because the clock runs whatever the creator did.
|
|
353
|
+
* The roster uses it to say "asked on …, again after …" instead of offering
|
|
354
|
+
* an ask that would not be sent. Absent once the clock has run out.
|
|
355
|
+
*/
|
|
356
|
+
reaskedAt?: string;
|
|
357
|
+
reaskAvailableAt?: string;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The platforms a creator has connected, as the agency side sees them
|
|
362
|
+
* (discovery, the grant's "reaches"). `GET users/me/connected-platforms`,
|
|
363
|
+
* answered to every creator, subscribed or not.
|
|
364
|
+
*/
|
|
365
|
+
export interface I_ConnectedPlatforms {
|
|
366
|
+
platforms: PublishPlatform[];
|
|
254
367
|
}
|
|
255
368
|
|
|
256
369
|
export interface I_AgencyRoster {
|
|
257
370
|
totals: {
|
|
258
371
|
creators: number;
|
|
259
372
|
active: number;
|
|
260
|
-
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Summed only over rows in AGENCY_ROSTER_METRICS_STATES whose creator holds
|
|
375
|
+
* `analytics`; disconnected platforms excluded. `null` when no counted
|
|
376
|
+
* creator has a synced number yet, never a zero standing in (T067).
|
|
377
|
+
*/
|
|
378
|
+
followers: number | null;
|
|
379
|
+
/**
|
|
380
|
+
* Same rows as `followers`, against the snapshot seven days earlier. `null`
|
|
381
|
+
* when no counted creator has a week-old point — unknown, never a zero.
|
|
382
|
+
*/
|
|
383
|
+
followersDelta7d: number | null;
|
|
384
|
+
/**
|
|
385
|
+
* Calendar week, Mon-Sun, organization timezone, over rows in
|
|
386
|
+
* AGENCY_ROSTER_METRICS_STATES whose creator holds `comments`. Like
|
|
387
|
+
* `followers`, `null` when no counted creator has a synced number yet:
|
|
388
|
+
* unknown, never a zero (SC-008, T067).
|
|
389
|
+
*/
|
|
390
|
+
commentsThisWeek: number | null;
|
|
265
391
|
/** Count of rows in AGENCY_ROSTER_NEEDS_ATTENTION_STATES. */
|
|
266
392
|
needsAttention: number;
|
|
267
393
|
};
|
|
268
394
|
items: I_AgencyRosterRow[];
|
|
269
395
|
}
|
|
270
396
|
|
|
397
|
+
/**
|
|
398
|
+
* A coverage that ENDED recently, so the creator meeting the paywall again is
|
|
399
|
+
* told why (FR-014: "an honest message, never a silent loss of access").
|
|
400
|
+
*/
|
|
401
|
+
export interface I_SponsorshipEnded {
|
|
402
|
+
organizationName: string;
|
|
403
|
+
endedAt: string;
|
|
404
|
+
}
|
|
405
|
+
|
|
271
406
|
/** What a sponsored creator sees on their OWN subscription screen (FR-014). */
|
|
272
407
|
export interface I_SponsoredBy {
|
|
273
408
|
organizationName: string;
|
|
@@ -276,45 +411,162 @@ export interface I_SponsoredBy {
|
|
|
276
411
|
until?: string;
|
|
277
412
|
}
|
|
278
413
|
|
|
414
|
+
// ── Billing (US4) ────────────────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
/** The card on file, as Stripe describes it. Never more than these four facts. */
|
|
417
|
+
export interface I_AgencyPaymentMethod {
|
|
418
|
+
brand: string;
|
|
419
|
+
last4: string;
|
|
420
|
+
expMonth: number;
|
|
421
|
+
expYear: number;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* One invoice. `amount` is Stripe's minor unit verbatim; the display edge
|
|
426
|
+
* formats it once, like every other price in the app.
|
|
427
|
+
*/
|
|
428
|
+
export interface I_AgencyInvoice {
|
|
429
|
+
id: string;
|
|
430
|
+
number: string | null;
|
|
431
|
+
date: string;
|
|
432
|
+
amount: number;
|
|
433
|
+
currency: string;
|
|
434
|
+
status: string;
|
|
435
|
+
pdfUrl?: string;
|
|
436
|
+
hostedUrl?: string;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* One creator on an ACTIVE grant, with whether the agency covers their plan
|
|
441
|
+
* (FR-013b). `tier: null` is "not covered". `until` is set while a coverage is
|
|
442
|
+
* ending: it runs to the close of the period already paid for.
|
|
443
|
+
*/
|
|
444
|
+
export interface I_AgencyCoveredCreator {
|
|
445
|
+
grantId: string;
|
|
446
|
+
creator: { id: string; handle: string };
|
|
447
|
+
tier: SponsoredTier | null;
|
|
448
|
+
since?: string;
|
|
449
|
+
until?: string;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* An ended relationship, for the past-creators history (FR-013b). `creator.id`
|
|
454
|
+
* is null once the creator's account was purged; the handle is the snapshot
|
|
455
|
+
* frozen at grant time.
|
|
456
|
+
*/
|
|
457
|
+
export interface I_AgencyPastCreator {
|
|
458
|
+
grantId: string;
|
|
459
|
+
creator: { id: string | null; handle: string; displayName?: string };
|
|
460
|
+
endedAt: string;
|
|
461
|
+
endedBy: CreatorAccessGrantRevokedBy;
|
|
462
|
+
coveredUntil?: string;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Everything the owner's billing section and panel show (FR-013, FR-013b).
|
|
467
|
+
*
|
|
468
|
+
* `seats` is the ONE home of the billed quantity: the count of active grants.
|
|
469
|
+
* Covering a creator never changes it (clarify 2026-09-17), so nothing here
|
|
470
|
+
* derives a price from `covered`.
|
|
471
|
+
*
|
|
472
|
+
* `price` is null when the seat product is not configured in this deployment
|
|
473
|
+
* (T031, the money gate); the screen says so rather than inventing a number.
|
|
474
|
+
* `nextInvoice` is Stripe's own preview, so a coupon is already applied to it.
|
|
475
|
+
*/
|
|
476
|
+
export interface I_AgencyBilling {
|
|
477
|
+
subscriptionStatus: AgencySubscriptionStatus;
|
|
478
|
+
/** Creators with an active grant. */
|
|
479
|
+
seats: number;
|
|
480
|
+
/**
|
|
481
|
+
* What Stripe bills: `max(1, seats)`. An agency with no creators yet still
|
|
482
|
+
* pays for one seat, and the screen must say the number Stripe charges.
|
|
483
|
+
*/
|
|
484
|
+
billedSeats: number;
|
|
485
|
+
price: { amount: number; currency: string; interval: string } | null;
|
|
486
|
+
currentPeriodEnd?: string;
|
|
487
|
+
cancelAtPeriodEnd?: boolean;
|
|
488
|
+
nextInvoice?: { amount: number; currency: string; date: string };
|
|
489
|
+
discount?: { name?: string; percentOff?: number; amountOff?: number };
|
|
490
|
+
paymentMethod?: I_AgencyPaymentMethod;
|
|
491
|
+
billingDetails?: { name?: string; email?: string; country?: string; taxId?: string };
|
|
492
|
+
invoices: I_AgencyInvoice[];
|
|
493
|
+
covered: I_AgencyCoveredCreator[];
|
|
494
|
+
pastCreators: I_AgencyPastCreator[];
|
|
495
|
+
}
|
|
496
|
+
|
|
279
497
|
// ── DTOs ─────────────────────────────────────────────────────────────────────
|
|
280
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Every DTO below carries class-validator decorators and a constructor that
|
|
501
|
+
* survives being called with nothing.
|
|
502
|
+
*
|
|
503
|
+
* Both are load-bearing and were missing until 2026-09-22. The backend mounts
|
|
504
|
+
* a global `ValidationPipe`, which validates the decorated metadata of the
|
|
505
|
+
* class a handler's `@Body()` is typed with — an undecorated class, or a body
|
|
506
|
+
* typed as a plain interface, is simply passed through unchecked. And the pipe
|
|
507
|
+
* instantiates the class with NO arguments, so a constructor that reads
|
|
508
|
+
* `data.name` would throw a TypeError on every request rather than validate
|
|
509
|
+
* anything. The rest of this library's DTOs take `data?: Partial<T>` for that
|
|
510
|
+
* reason; these now do too.
|
|
511
|
+
*/
|
|
512
|
+
|
|
281
513
|
export class CreateAgencyOrganizationDto {
|
|
514
|
+
@IsString()
|
|
515
|
+
@IsNotEmpty()
|
|
282
516
|
name: string;
|
|
517
|
+
|
|
518
|
+
/** Rendered as a link on the admin screen, so the protocol is checked here. */
|
|
519
|
+
@IsOptional()
|
|
520
|
+
@IsUrl({ protocols: ['http', 'https'], require_protocol: true })
|
|
283
521
|
website?: string;
|
|
522
|
+
|
|
523
|
+
@IsString()
|
|
524
|
+
@IsNotEmpty()
|
|
284
525
|
registryId: string;
|
|
526
|
+
|
|
527
|
+
/** ISO-3166 alpha-2: which register a reviewer should look in. */
|
|
528
|
+
@IsString()
|
|
529
|
+
@Length(2, 2)
|
|
285
530
|
registryCountry: string;
|
|
531
|
+
|
|
532
|
+
@IsEmail()
|
|
286
533
|
contactEmail: string;
|
|
287
534
|
|
|
288
|
-
constructor(data
|
|
289
|
-
this.name = data
|
|
290
|
-
this.website = data
|
|
291
|
-
this.registryId = data
|
|
292
|
-
this.registryCountry = data
|
|
293
|
-
this.contactEmail = data
|
|
535
|
+
constructor(data?: Partial<CreateAgencyOrganizationDto>) {
|
|
536
|
+
this.name = data?.name ?? '';
|
|
537
|
+
this.website = data?.website;
|
|
538
|
+
this.registryId = data?.registryId ?? '';
|
|
539
|
+
this.registryCountry = data?.registryCountry ?? '';
|
|
540
|
+
this.contactEmail = data?.contactEmail ?? '';
|
|
294
541
|
}
|
|
295
542
|
}
|
|
296
543
|
|
|
297
544
|
export class InviteAgencyMemberDto {
|
|
545
|
+
@IsEmail()
|
|
298
546
|
email: string;
|
|
299
547
|
|
|
300
|
-
constructor(data
|
|
301
|
-
this.email = data
|
|
548
|
+
constructor(data?: Partial<InviteAgencyMemberDto>) {
|
|
549
|
+
this.email = data?.email ?? '';
|
|
302
550
|
}
|
|
303
551
|
}
|
|
304
552
|
|
|
305
553
|
export class RedeemAgencyInviteDto {
|
|
554
|
+
@IsString()
|
|
555
|
+
@IsNotEmpty()
|
|
306
556
|
token: string;
|
|
307
557
|
|
|
308
|
-
constructor(data
|
|
309
|
-
this.token = data
|
|
558
|
+
constructor(data?: Partial<RedeemAgencyInviteDto>) {
|
|
559
|
+
this.token = data?.token ?? '';
|
|
310
560
|
}
|
|
311
561
|
}
|
|
312
562
|
|
|
313
563
|
export class AcceptAgencyMemberInviteDto {
|
|
564
|
+
@IsString()
|
|
565
|
+
@IsNotEmpty()
|
|
314
566
|
token: string;
|
|
315
567
|
|
|
316
|
-
constructor(data
|
|
317
|
-
this.token = data
|
|
568
|
+
constructor(data?: Partial<AcceptAgencyMemberInviteDto>) {
|
|
569
|
+
this.token = data?.token ?? '';
|
|
318
570
|
}
|
|
319
571
|
}
|
|
320
572
|
|
|
@@ -323,60 +575,137 @@ export class AcceptAgencyMemberInviteDto {
|
|
|
323
575
|
* is the creator's answer and defaults to the requested set when absent.
|
|
324
576
|
*/
|
|
325
577
|
export class AcceptGrantDto {
|
|
578
|
+
@IsOptional()
|
|
579
|
+
@IsString()
|
|
580
|
+
@IsNotEmpty()
|
|
326
581
|
token?: string;
|
|
582
|
+
|
|
583
|
+
@IsOptional()
|
|
584
|
+
@IsUUID()
|
|
327
585
|
grantId?: string;
|
|
586
|
+
|
|
587
|
+
/** A subset of the three reads. Never empty: sharing nothing is declining. */
|
|
588
|
+
@IsOptional()
|
|
589
|
+
@IsArray()
|
|
590
|
+
@ArrayNotEmpty()
|
|
591
|
+
@IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
|
|
328
592
|
scopes?: I_AgencyGrantScope[];
|
|
329
593
|
|
|
330
|
-
constructor(data
|
|
331
|
-
this.token = data
|
|
332
|
-
this.grantId = data
|
|
333
|
-
this.scopes = data
|
|
594
|
+
constructor(data?: Partial<AcceptGrantDto>) {
|
|
595
|
+
this.token = data?.token;
|
|
596
|
+
this.grantId = data?.grantId;
|
|
597
|
+
this.scopes = data?.scopes;
|
|
334
598
|
}
|
|
335
599
|
}
|
|
336
600
|
|
|
337
601
|
/** The creator narrowing or widening a live grant — the only other write path. */
|
|
338
602
|
export class UpdateGrantScopesDto {
|
|
603
|
+
@IsArray()
|
|
604
|
+
@ArrayNotEmpty()
|
|
605
|
+
@IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
|
|
339
606
|
scopes: I_AgencyGrantScope[];
|
|
340
607
|
|
|
341
|
-
constructor(data
|
|
342
|
-
this.scopes = data
|
|
608
|
+
constructor(data?: Partial<UpdateGrantScopesDto>) {
|
|
609
|
+
this.scopes = data?.scopes ?? [];
|
|
343
610
|
}
|
|
344
611
|
}
|
|
345
612
|
|
|
346
613
|
export class SponsorCreatorDto {
|
|
614
|
+
@IsIn(['starter', 'pro'])
|
|
347
615
|
tier: SponsoredTier;
|
|
348
616
|
|
|
349
|
-
constructor(data
|
|
350
|
-
this.tier = data
|
|
617
|
+
constructor(data?: Partial<SponsorCreatorDto>) {
|
|
618
|
+
this.tier = data?.tier ?? 'starter';
|
|
351
619
|
}
|
|
352
620
|
}
|
|
353
621
|
|
|
354
622
|
export class AgencyDiscoverQueryDto {
|
|
355
623
|
/** An EXACT platform username or e-mail. Never recorded in analytics (FR-015). */
|
|
624
|
+
@IsString()
|
|
625
|
+
@IsNotEmpty()
|
|
356
626
|
q: string;
|
|
357
627
|
|
|
358
|
-
constructor(data
|
|
359
|
-
this.q = data
|
|
628
|
+
constructor(data?: Partial<AgencyDiscoverQueryDto>) {
|
|
629
|
+
this.q = data?.q ?? '';
|
|
360
630
|
}
|
|
361
631
|
}
|
|
362
632
|
|
|
363
633
|
export class AgencyAccessRequestDto {
|
|
634
|
+
/**
|
|
635
|
+
* A user id: a UUID or a Firebase uid (letters, digits, hyphens). It was
|
|
636
|
+
* `@IsUUID()` until US5 was built, which refused most real creators — their
|
|
637
|
+
* ids are Firebase uids.
|
|
638
|
+
*/
|
|
639
|
+
@IsString()
|
|
640
|
+
@Matches(/^[A-Za-z0-9-]{1,128}$/)
|
|
364
641
|
userId: string;
|
|
642
|
+
|
|
643
|
+
@IsArray()
|
|
644
|
+
@ArrayNotEmpty()
|
|
645
|
+
@IsIn(AGENCY_GRANT_SCOPES as unknown as string[], { each: true })
|
|
365
646
|
scopes: I_AgencyGrantScope[];
|
|
366
647
|
|
|
367
|
-
constructor(data
|
|
368
|
-
this.userId = data
|
|
369
|
-
this.scopes = data
|
|
648
|
+
constructor(data?: Partial<AgencyAccessRequestDto>) {
|
|
649
|
+
this.userId = data?.userId ?? '';
|
|
650
|
+
this.scopes = data?.scopes ?? [];
|
|
370
651
|
}
|
|
371
652
|
}
|
|
372
653
|
|
|
654
|
+
/** `PATCH /users/me/agency-discoverable` — "Let agencies find me" (FR-015). */
|
|
655
|
+
export class UpdateAgencyDiscoverableDto {
|
|
656
|
+
@IsBoolean()
|
|
657
|
+
discoverable: boolean;
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* NO default: a required boolean that defaults to false lets an empty body
|
|
661
|
+
* through validation and reach the database as `undefined` (review M7).
|
|
662
|
+
*/
|
|
663
|
+
constructor(data?: Partial<UpdateAgencyDiscoverableDto>) {
|
|
664
|
+
if (data?.discoverable !== undefined) this.discoverable = data.discoverable;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* One discover hit: the public handle and the platforms, NEVER a metric and
|
|
670
|
+
* never a grant flag, so the payload cannot tell a caller whether it already
|
|
671
|
+
* holds the creator (FR-015). `userId` is what the request is sent for.
|
|
672
|
+
*/
|
|
673
|
+
export interface I_AgencyDiscoverHit {
|
|
674
|
+
userId: string;
|
|
675
|
+
handle: string;
|
|
676
|
+
platforms: PublishPlatform[];
|
|
677
|
+
/**
|
|
678
|
+
* The platform whose username matched. Absent on an e-mail match. Two hits
|
|
679
|
+
* with the same handle are two different people (@mark on YouTube, another
|
|
680
|
+
* @mark on Instagram), and this is what tells them apart (T068).
|
|
681
|
+
*/
|
|
682
|
+
matchedPlatform?: PublishPlatform;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* `POST /agency/roster/discover` — the SAME shape and status for every case:
|
|
687
|
+
* discoverable exact matches, a non-discoverable account, a partial match and
|
|
688
|
+
* no account at all (SC-009). A miss is `{ results: [] }`, never a 404. An
|
|
689
|
+
* exact handle held by several findable creators returns each of them, one
|
|
690
|
+
* row per person (Maciej, 2026-09-26, T068); an e-mail matches one person.
|
|
691
|
+
*/
|
|
692
|
+
export interface I_AgencyDiscoverResult {
|
|
693
|
+
results: I_AgencyDiscoverHit[];
|
|
694
|
+
}
|
|
695
|
+
|
|
373
696
|
export class AdminSetOrganizationStatusDto {
|
|
697
|
+
@IsIn(['approved', 'rejected'])
|
|
374
698
|
status: Extract<AgencyOrganizationStatus, 'approved' | 'rejected'>;
|
|
375
|
-
|
|
699
|
+
|
|
700
|
+
/** Required on reject — a rejected applicant is shown this (FR-002). The
|
|
701
|
+
* "required when rejecting" half is a rule about the PAIR, so it stays in
|
|
702
|
+
* the service where both fields are in hand. */
|
|
703
|
+
@IsOptional()
|
|
704
|
+
@IsString()
|
|
376
705
|
reason?: string;
|
|
377
706
|
|
|
378
|
-
constructor(data
|
|
379
|
-
this.status = data
|
|
380
|
-
this.reason = data
|
|
707
|
+
constructor(data?: Partial<AdminSetOrganizationStatusDto>) {
|
|
708
|
+
this.status = data?.status ?? 'approved';
|
|
709
|
+
this.reason = data?.reason;
|
|
381
710
|
}
|
|
382
711
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* and the customer portal redirect.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { I_SponsoredBy, I_SponsorshipEnded } from '../agency';
|
|
9
|
+
|
|
8
10
|
// ── SchedulingSubscriptionTier ────────────────────────────────────────────────
|
|
9
11
|
|
|
10
12
|
export type SchedulingSubscriptionTier = 'free' | 'starter' | 'pro';
|
|
@@ -23,6 +25,7 @@ export type SchedulingSubscriptionInfoInput = Pick<
|
|
|
23
25
|
SchedulingSubscriptionInfoDTO,
|
|
24
26
|
'tier' | 'status' | 'expiresAt'
|
|
25
27
|
> &
|
|
28
|
+
Partial<Pick<SchedulingSubscriptionInfoDTO, 'sponsoredBy' | 'sponsorshipEnded'>> &
|
|
26
29
|
(
|
|
27
30
|
| { maxAccounts: number; maxPlatforms?: number }
|
|
28
31
|
| {
|
|
@@ -46,6 +49,14 @@ export class SchedulingSubscriptionInfoDTO {
|
|
|
46
49
|
* Read `maxAccounts`. Always equal to it; removed in a later release.
|
|
47
50
|
*/
|
|
48
51
|
maxPlatforms: number;
|
|
52
|
+
/**
|
|
53
|
+
* Present only while an agency covers this creator's plan (spec 175 FR-014).
|
|
54
|
+
* `until` appears once the sponsorship is ending: the date the plan runs to.
|
|
55
|
+
* Absent — never null — for everyone else, so "not sponsored" has one shape.
|
|
56
|
+
*/
|
|
57
|
+
sponsoredBy?: I_SponsoredBy;
|
|
58
|
+
/** Present only when a coverage ended recently and nothing replaced it. Absent otherwise. */
|
|
59
|
+
sponsorshipEnded?: I_SponsorshipEnded;
|
|
49
60
|
|
|
50
61
|
constructor(data: SchedulingSubscriptionInfoInput) {
|
|
51
62
|
const maxAccounts = 'maxAccounts' in data ? data.maxAccounts : data.maxPlatforms;
|
|
@@ -54,6 +65,8 @@ export class SchedulingSubscriptionInfoDTO {
|
|
|
54
65
|
this.expiresAt = data.expiresAt;
|
|
55
66
|
this.maxAccounts = maxAccounts;
|
|
56
67
|
this.maxPlatforms = maxAccounts;
|
|
68
|
+
if (data.sponsoredBy) this.sponsoredBy = data.sponsoredBy;
|
|
69
|
+
if (data.sponsorshipEnded) this.sponsorshipEnded = data.sponsorshipEnded;
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
72
|
|