ad2app-lib 1.44.1 → 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 +248 -6
- package/dist/types/agency.js +52 -2
- package/dist/types/scheduling/I_SchedulingProUpgrade.d.ts +10 -1
- package/dist/types/scheduling/I_SchedulingProUpgrade.js +4 -0
- package/package.json +2 -2
- 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 +21 -1
- package/src/types/agency.ts +259 -6
- package/src/types/scheduling/I_SchedulingProUpgrade.ts +13 -0
|
@@ -79,6 +79,25 @@ export declare const ACCESS_DENIAL_CODES: {
|
|
|
79
79
|
* per person, FR-004). Names the case and NOTHING else about that account.
|
|
80
80
|
*/
|
|
81
81
|
readonly AGENCY_ALREADY_IN_ORGANIZATION: "agency-already-in-organization";
|
|
82
|
+
/**
|
|
83
|
+
* The owner invited an address that is ALREADY on this organization's team,
|
|
84
|
+
* themselves included (T063). A different case from the one above: the owner
|
|
85
|
+
* is told this person is already in, not that they belong elsewhere.
|
|
86
|
+
*/
|
|
87
|
+
readonly AGENCY_ALREADY_MEMBER: "agency-already-member";
|
|
88
|
+
/**
|
|
89
|
+
* The owner tried to cover a creator who pays for their own plan. Covering
|
|
90
|
+
* is for a creator without a paid plan (US4 scenario 3); covering a payer
|
|
91
|
+
* would leave them paying for what the agency also pays for (T066).
|
|
92
|
+
*/
|
|
93
|
+
readonly AGENCY_CREATOR_PAYS_OWN_PLAN: "agency-creator-pays-own-plan";
|
|
94
|
+
/**
|
|
95
|
+
* A creator whose plan an agency covers asked for the billing portal, and
|
|
96
|
+
* has no Stripe customer of their own to open it for (FR-014). The body
|
|
97
|
+
* names the covering agency; the client hides the portal button instead of
|
|
98
|
+
* showing a Stripe error.
|
|
99
|
+
*/
|
|
100
|
+
readonly SPONSORED_NO_PORTAL: "sponsored-no-portal";
|
|
82
101
|
};
|
|
83
102
|
export type AccessDenialCode = (typeof ACCESS_DENIAL_CODES)[keyof typeof ACCESS_DENIAL_CODES];
|
|
84
103
|
/**
|
|
@@ -83,6 +83,25 @@ exports.ACCESS_DENIAL_CODES = {
|
|
|
83
83
|
* per person, FR-004). Names the case and NOTHING else about that account.
|
|
84
84
|
*/
|
|
85
85
|
AGENCY_ALREADY_IN_ORGANIZATION: "agency-already-in-organization",
|
|
86
|
+
/**
|
|
87
|
+
* The owner invited an address that is ALREADY on this organization's team,
|
|
88
|
+
* themselves included (T063). A different case from the one above: the owner
|
|
89
|
+
* is told this person is already in, not that they belong elsewhere.
|
|
90
|
+
*/
|
|
91
|
+
AGENCY_ALREADY_MEMBER: "agency-already-member",
|
|
92
|
+
/**
|
|
93
|
+
* The owner tried to cover a creator who pays for their own plan. Covering
|
|
94
|
+
* is for a creator without a paid plan (US4 scenario 3); covering a payer
|
|
95
|
+
* would leave them paying for what the agency also pays for (T066).
|
|
96
|
+
*/
|
|
97
|
+
AGENCY_CREATOR_PAYS_OWN_PLAN: "agency-creator-pays-own-plan",
|
|
98
|
+
/**
|
|
99
|
+
* A creator whose plan an agency covers asked for the billing portal, and
|
|
100
|
+
* has no Stripe customer of their own to open it for (FR-014). The body
|
|
101
|
+
* names the covering agency; the client hides the portal button instead of
|
|
102
|
+
* showing a Stripe error.
|
|
103
|
+
*/
|
|
104
|
+
SPONSORED_NO_PORTAL: "sponsored-no-portal",
|
|
86
105
|
};
|
|
87
106
|
/**
|
|
88
107
|
* WHICH precondition the entitlement wall refused on (backend#305, spec 170).
|
package/dist/types/agency.d.ts
CHANGED
|
@@ -93,6 +93,12 @@ export interface I_AgencyMember {
|
|
|
93
93
|
/** Null while an e-mail invitation is outstanding — nobody has accepted yet. */
|
|
94
94
|
userId?: string;
|
|
95
95
|
invitedEmail: string;
|
|
96
|
+
/**
|
|
97
|
+
* The account's own e-mail once the invitation is accepted (and always for
|
|
98
|
+
* the owner, who applied rather than being invited). The Team panel names a
|
|
99
|
+
* person by this, never by the organization's contact address (T063).
|
|
100
|
+
*/
|
|
101
|
+
accountEmail?: string;
|
|
96
102
|
role: AgencyMemberRole;
|
|
97
103
|
invitedAt: string;
|
|
98
104
|
/** "Who is in" is exactly `acceptedAt != null`. */
|
|
@@ -152,7 +158,27 @@ export interface I_CreatorAccessGrant {
|
|
|
152
158
|
};
|
|
153
159
|
/** Which platforms the grant reaches right now — derived at read time, never stored. */
|
|
154
160
|
platformsReached: PublishPlatform[];
|
|
161
|
+
/**
|
|
162
|
+
* The agency's OPEN second ask on an active grant, for scopes the creator
|
|
163
|
+
* did not grant (FR-008). Kept apart from `requestedScopes`, so the consent
|
|
164
|
+
* screen names exactly this ask and nothing refused earlier (review H4).
|
|
165
|
+
* Absent when there is no open ask.
|
|
166
|
+
*/
|
|
167
|
+
reaskedScopes?: I_AgencyGrantScope[];
|
|
168
|
+
/** When that open ask expires unanswered. */
|
|
169
|
+
reaskExpiresAt?: string;
|
|
155
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Outcomes of discovery and requests that are not refusals of access, sent as
|
|
173
|
+
* codes so the screen composes the sentence in the reader's language (FR-019,
|
|
174
|
+
* review M4). A 429 carries `{ code, limit, windowMinutes }`.
|
|
175
|
+
*/
|
|
176
|
+
export declare const AGENCY_REQUEST_CODES: {
|
|
177
|
+
readonly RATE_LIMITED: "agency-rate-limited";
|
|
178
|
+
readonly REQUEST_EXPIRED: "agency-request-expired";
|
|
179
|
+
readonly REQUEST_ANSWERED: "agency-request-answered";
|
|
180
|
+
};
|
|
181
|
+
export type AgencyRequestCode = (typeof AGENCY_REQUEST_CODES)[keyof typeof AGENCY_REQUEST_CODES];
|
|
156
182
|
/**
|
|
157
183
|
* Roster row states, DECLARED IN PRECEDENCE ORDER (data-model §Derived).
|
|
158
184
|
*
|
|
@@ -160,7 +186,7 @@ export interface I_CreatorAccessGrant {
|
|
|
160
186
|
* first. `revoked` is deliberately absent — a revoked creator leaves the roster
|
|
161
187
|
* entirely for the settings history (FR-011).
|
|
162
188
|
*/
|
|
163
|
-
export declare const AGENCY_ROSTER_ROW_STATES: readonly ["requested", "lapsed", "disconnected", "stale", "active"];
|
|
189
|
+
export declare const AGENCY_ROSTER_ROW_STATES: readonly ["requested", "lapsed", "disconnected", "syncing", "stale", "active"];
|
|
164
190
|
export type AgencyRosterRowState = (typeof AGENCY_ROSTER_ROW_STATES)[number];
|
|
165
191
|
/**
|
|
166
192
|
* What "needs attention" means — ONE home for the KPI tile, the roster segment
|
|
@@ -171,6 +197,32 @@ export type AgencyRosterRowState = (typeof AGENCY_ROSTER_ROW_STATES)[number];
|
|
|
171
197
|
*/
|
|
172
198
|
export declare const AGENCY_ROSTER_NEEDS_ATTENTION_STATES: readonly ["lapsed", "disconnected", "stale"];
|
|
173
199
|
export type AgencyRosterNeedsAttentionState = (typeof AGENCY_ROSTER_NEEDS_ATTENTION_STATES)[number];
|
|
200
|
+
/**
|
|
201
|
+
* The rows that carry numbers — ONE home for "which creators' numbers may the
|
|
202
|
+
* agency see on the roster" (decided 2026-09-24, review M1/M2).
|
|
203
|
+
*
|
|
204
|
+
* Only a row whose card OPENS carries `metrics` and counts in the followers,
|
|
205
|
+
* followers-delta and comments totals: `active`, and `stale` (the card opens;
|
|
206
|
+
* the numbers are as old as `lastSyncAt` says). A `lapsed` creator's reads are
|
|
207
|
+
* refused on the card, so their numbers must not reach the agency through the
|
|
208
|
+
* roster instead; a `disconnected` creator has nothing live to count; a
|
|
209
|
+
* `requested` creator has shared nothing yet.
|
|
210
|
+
*/
|
|
211
|
+
export declare const AGENCY_ROSTER_METRICS_STATES: readonly ["syncing", "stale", "active"];
|
|
212
|
+
export type AgencyRosterMetricsState = (typeof AGENCY_ROSTER_METRICS_STATES)[number];
|
|
213
|
+
/**
|
|
214
|
+
* One creator's numbers on the roster (FR-011a: sortable metric columns), read
|
|
215
|
+
* from the daily-synced tables, never a vendor call. `null` is a REASON, never
|
|
216
|
+
* a zero: the creator did not grant that scope, or nothing has been synced yet.
|
|
217
|
+
*/
|
|
218
|
+
export interface I_AgencyRosterRowMetrics {
|
|
219
|
+
/** Latest followers over the creator's CONNECTED platforms; null without `analytics` or before the first sync. */
|
|
220
|
+
followers: number | null;
|
|
221
|
+
/** Against the snapshot seven days earlier, per connected platform; null without `analytics` or without a week-old point. */
|
|
222
|
+
followersDelta7d: number | null;
|
|
223
|
+
/** The current Monday-to-Sunday week, organization timezone; null without `comments`. */
|
|
224
|
+
commentsThisWeek: number | null;
|
|
225
|
+
}
|
|
174
226
|
export interface I_AgencyRosterRow {
|
|
175
227
|
grantId: string;
|
|
176
228
|
creator: {
|
|
@@ -180,22 +232,72 @@ export interface I_AgencyRosterRow {
|
|
|
180
232
|
platforms: PublishPlatform[];
|
|
181
233
|
state: AgencyRosterRowState;
|
|
182
234
|
grantedScopes: I_AgencyGrantScope[];
|
|
235
|
+
/**
|
|
236
|
+
* The newest successful daily sync. Absent on a `requested` row (nothing is
|
|
237
|
+
* shared yet, so there is nothing to have synced for the agency) and on a
|
|
238
|
+
* creator never synced. `platforms` stays on a requested row: it is what the
|
|
239
|
+
* consent screen names, a fact about the account rather than shared data.
|
|
240
|
+
*/
|
|
183
241
|
lastSyncAt?: string;
|
|
242
|
+
/**
|
|
243
|
+
* Present ONLY on rows in AGENCY_ROSTER_METRICS_STATES (`active`, `stale`):
|
|
244
|
+
* the rows whose card opens. Absent on `lapsed`, `disconnected` and
|
|
245
|
+
* `requested` rows — no numbers, not null numbers.
|
|
246
|
+
*/
|
|
247
|
+
metrics?: I_AgencyRosterRowMetrics;
|
|
248
|
+
/**
|
|
249
|
+
* The agency's own last "Ask again" (FR-008), while its one-per-TTL clock
|
|
250
|
+
* runs (audit F19): when it was sent and when another is possible. It says
|
|
251
|
+
* nothing about the ANSWER, because the clock runs whatever the creator did.
|
|
252
|
+
* The roster uses it to say "asked on …, again after …" instead of offering
|
|
253
|
+
* an ask that would not be sent. Absent once the clock has run out.
|
|
254
|
+
*/
|
|
255
|
+
reaskedAt?: string;
|
|
256
|
+
reaskAvailableAt?: string;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The platforms a creator has connected, as the agency side sees them
|
|
260
|
+
* (discovery, the grant's "reaches"). `GET users/me/connected-platforms`,
|
|
261
|
+
* answered to every creator, subscribed or not.
|
|
262
|
+
*/
|
|
263
|
+
export interface I_ConnectedPlatforms {
|
|
264
|
+
platforms: PublishPlatform[];
|
|
184
265
|
}
|
|
185
266
|
export interface I_AgencyRoster {
|
|
186
267
|
totals: {
|
|
187
268
|
creators: number;
|
|
188
269
|
active: number;
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Summed only over rows in AGENCY_ROSTER_METRICS_STATES whose creator holds
|
|
272
|
+
* `analytics`; disconnected platforms excluded. `null` when no counted
|
|
273
|
+
* creator has a synced number yet, never a zero standing in (T067).
|
|
274
|
+
*/
|
|
275
|
+
followers: number | null;
|
|
276
|
+
/**
|
|
277
|
+
* Same rows as `followers`, against the snapshot seven days earlier. `null`
|
|
278
|
+
* when no counted creator has a week-old point — unknown, never a zero.
|
|
279
|
+
*/
|
|
280
|
+
followersDelta7d: number | null;
|
|
281
|
+
/**
|
|
282
|
+
* Calendar week, Mon-Sun, organization timezone, over rows in
|
|
283
|
+
* AGENCY_ROSTER_METRICS_STATES whose creator holds `comments`. Like
|
|
284
|
+
* `followers`, `null` when no counted creator has a synced number yet:
|
|
285
|
+
* unknown, never a zero (SC-008, T067).
|
|
286
|
+
*/
|
|
287
|
+
commentsThisWeek: number | null;
|
|
194
288
|
/** Count of rows in AGENCY_ROSTER_NEEDS_ATTENTION_STATES. */
|
|
195
289
|
needsAttention: number;
|
|
196
290
|
};
|
|
197
291
|
items: I_AgencyRosterRow[];
|
|
198
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* A coverage that ENDED recently, so the creator meeting the paywall again is
|
|
295
|
+
* told why (FR-014: "an honest message, never a silent loss of access").
|
|
296
|
+
*/
|
|
297
|
+
export interface I_SponsorshipEnded {
|
|
298
|
+
organizationName: string;
|
|
299
|
+
endedAt: string;
|
|
300
|
+
}
|
|
199
301
|
/** What a sponsored creator sees on their OWN subscription screen (FR-014). */
|
|
200
302
|
export interface I_SponsoredBy {
|
|
201
303
|
organizationName: string;
|
|
@@ -203,6 +305,106 @@ export interface I_SponsoredBy {
|
|
|
203
305
|
/** Present once the sponsorship is ending: the date their plan runs to. */
|
|
204
306
|
until?: string;
|
|
205
307
|
}
|
|
308
|
+
/** The card on file, as Stripe describes it. Never more than these four facts. */
|
|
309
|
+
export interface I_AgencyPaymentMethod {
|
|
310
|
+
brand: string;
|
|
311
|
+
last4: string;
|
|
312
|
+
expMonth: number;
|
|
313
|
+
expYear: number;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* One invoice. `amount` is Stripe's minor unit verbatim; the display edge
|
|
317
|
+
* formats it once, like every other price in the app.
|
|
318
|
+
*/
|
|
319
|
+
export interface I_AgencyInvoice {
|
|
320
|
+
id: string;
|
|
321
|
+
number: string | null;
|
|
322
|
+
date: string;
|
|
323
|
+
amount: number;
|
|
324
|
+
currency: string;
|
|
325
|
+
status: string;
|
|
326
|
+
pdfUrl?: string;
|
|
327
|
+
hostedUrl?: string;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* One creator on an ACTIVE grant, with whether the agency covers their plan
|
|
331
|
+
* (FR-013b). `tier: null` is "not covered". `until` is set while a coverage is
|
|
332
|
+
* ending: it runs to the close of the period already paid for.
|
|
333
|
+
*/
|
|
334
|
+
export interface I_AgencyCoveredCreator {
|
|
335
|
+
grantId: string;
|
|
336
|
+
creator: {
|
|
337
|
+
id: string;
|
|
338
|
+
handle: string;
|
|
339
|
+
};
|
|
340
|
+
tier: SponsoredTier | null;
|
|
341
|
+
since?: string;
|
|
342
|
+
until?: string;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* An ended relationship, for the past-creators history (FR-013b). `creator.id`
|
|
346
|
+
* is null once the creator's account was purged; the handle is the snapshot
|
|
347
|
+
* frozen at grant time.
|
|
348
|
+
*/
|
|
349
|
+
export interface I_AgencyPastCreator {
|
|
350
|
+
grantId: string;
|
|
351
|
+
creator: {
|
|
352
|
+
id: string | null;
|
|
353
|
+
handle: string;
|
|
354
|
+
displayName?: string;
|
|
355
|
+
};
|
|
356
|
+
endedAt: string;
|
|
357
|
+
endedBy: CreatorAccessGrantRevokedBy;
|
|
358
|
+
coveredUntil?: string;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Everything the owner's billing section and panel show (FR-013, FR-013b).
|
|
362
|
+
*
|
|
363
|
+
* `seats` is the ONE home of the billed quantity: the count of active grants.
|
|
364
|
+
* Covering a creator never changes it (clarify 2026-09-17), so nothing here
|
|
365
|
+
* derives a price from `covered`.
|
|
366
|
+
*
|
|
367
|
+
* `price` is null when the seat product is not configured in this deployment
|
|
368
|
+
* (T031, the money gate); the screen says so rather than inventing a number.
|
|
369
|
+
* `nextInvoice` is Stripe's own preview, so a coupon is already applied to it.
|
|
370
|
+
*/
|
|
371
|
+
export interface I_AgencyBilling {
|
|
372
|
+
subscriptionStatus: AgencySubscriptionStatus;
|
|
373
|
+
/** Creators with an active grant. */
|
|
374
|
+
seats: number;
|
|
375
|
+
/**
|
|
376
|
+
* What Stripe bills: `max(1, seats)`. An agency with no creators yet still
|
|
377
|
+
* pays for one seat, and the screen must say the number Stripe charges.
|
|
378
|
+
*/
|
|
379
|
+
billedSeats: number;
|
|
380
|
+
price: {
|
|
381
|
+
amount: number;
|
|
382
|
+
currency: string;
|
|
383
|
+
interval: string;
|
|
384
|
+
} | null;
|
|
385
|
+
currentPeriodEnd?: string;
|
|
386
|
+
cancelAtPeriodEnd?: boolean;
|
|
387
|
+
nextInvoice?: {
|
|
388
|
+
amount: number;
|
|
389
|
+
currency: string;
|
|
390
|
+
date: string;
|
|
391
|
+
};
|
|
392
|
+
discount?: {
|
|
393
|
+
name?: string;
|
|
394
|
+
percentOff?: number;
|
|
395
|
+
amountOff?: number;
|
|
396
|
+
};
|
|
397
|
+
paymentMethod?: I_AgencyPaymentMethod;
|
|
398
|
+
billingDetails?: {
|
|
399
|
+
name?: string;
|
|
400
|
+
email?: string;
|
|
401
|
+
country?: string;
|
|
402
|
+
taxId?: string;
|
|
403
|
+
};
|
|
404
|
+
invoices: I_AgencyInvoice[];
|
|
405
|
+
covered: I_AgencyCoveredCreator[];
|
|
406
|
+
pastCreators: I_AgencyPastCreator[];
|
|
407
|
+
}
|
|
206
408
|
/**
|
|
207
409
|
* Every DTO below carries class-validator decorators and a constructor that
|
|
208
410
|
* survives being called with nothing.
|
|
@@ -264,10 +466,50 @@ export declare class AgencyDiscoverQueryDto {
|
|
|
264
466
|
constructor(data?: Partial<AgencyDiscoverQueryDto>);
|
|
265
467
|
}
|
|
266
468
|
export declare class AgencyAccessRequestDto {
|
|
469
|
+
/**
|
|
470
|
+
* A user id: a UUID or a Firebase uid (letters, digits, hyphens). It was
|
|
471
|
+
* `@IsUUID()` until US5 was built, which refused most real creators — their
|
|
472
|
+
* ids are Firebase uids.
|
|
473
|
+
*/
|
|
267
474
|
userId: string;
|
|
268
475
|
scopes: I_AgencyGrantScope[];
|
|
269
476
|
constructor(data?: Partial<AgencyAccessRequestDto>);
|
|
270
477
|
}
|
|
478
|
+
/** `PATCH /users/me/agency-discoverable` — "Let agencies find me" (FR-015). */
|
|
479
|
+
export declare class UpdateAgencyDiscoverableDto {
|
|
480
|
+
discoverable: boolean;
|
|
481
|
+
/**
|
|
482
|
+
* NO default: a required boolean that defaults to false lets an empty body
|
|
483
|
+
* through validation and reach the database as `undefined` (review M7).
|
|
484
|
+
*/
|
|
485
|
+
constructor(data?: Partial<UpdateAgencyDiscoverableDto>);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* One discover hit: the public handle and the platforms, NEVER a metric and
|
|
489
|
+
* never a grant flag, so the payload cannot tell a caller whether it already
|
|
490
|
+
* holds the creator (FR-015). `userId` is what the request is sent for.
|
|
491
|
+
*/
|
|
492
|
+
export interface I_AgencyDiscoverHit {
|
|
493
|
+
userId: string;
|
|
494
|
+
handle: string;
|
|
495
|
+
platforms: PublishPlatform[];
|
|
496
|
+
/**
|
|
497
|
+
* The platform whose username matched. Absent on an e-mail match. Two hits
|
|
498
|
+
* with the same handle are two different people (@mark on YouTube, another
|
|
499
|
+
* @mark on Instagram), and this is what tells them apart (T068).
|
|
500
|
+
*/
|
|
501
|
+
matchedPlatform?: PublishPlatform;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* `POST /agency/roster/discover` — the SAME shape and status for every case:
|
|
505
|
+
* discoverable exact matches, a non-discoverable account, a partial match and
|
|
506
|
+
* no account at all (SC-009). A miss is `{ results: [] }`, never a 404. An
|
|
507
|
+
* exact handle held by several findable creators returns each of them, one
|
|
508
|
+
* row per person (Maciej, 2026-09-26, T068); an e-mail matches one person.
|
|
509
|
+
*/
|
|
510
|
+
export interface I_AgencyDiscoverResult {
|
|
511
|
+
results: I_AgencyDiscoverHit[];
|
|
512
|
+
}
|
|
271
513
|
export declare class AdminSetOrganizationStatusDto {
|
|
272
514
|
status: Extract<AgencyOrganizationStatus, 'approved' | 'rejected'>;
|
|
273
515
|
/** Required on reject — a rejected applicant is shown this (FR-002). The
|
package/dist/types/agency.js
CHANGED
|
@@ -9,7 +9,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.AdminSetOrganizationStatusDto = exports.AgencyAccessRequestDto = exports.AgencyDiscoverQueryDto = exports.SponsorCreatorDto = exports.UpdateGrantScopesDto = exports.AcceptGrantDto = exports.AcceptAgencyMemberInviteDto = exports.RedeemAgencyInviteDto = exports.InviteAgencyMemberDto = exports.CreateAgencyOrganizationDto = exports.AGENCY_ROSTER_NEEDS_ATTENTION_STATES = exports.AGENCY_ROSTER_ROW_STATES = exports.CREATOR_ACCESS_GRANT_STATES = exports.AGENCY_ORGANIZATION_STATUSES = exports.isAgencyGrantScope = exports.AGENCY_GRANT_SCOPES = exports.AGENCY_ROUTE_PATHS = void 0;
|
|
12
|
+
exports.AdminSetOrganizationStatusDto = exports.UpdateAgencyDiscoverableDto = exports.AgencyAccessRequestDto = exports.AgencyDiscoverQueryDto = exports.SponsorCreatorDto = exports.UpdateGrantScopesDto = exports.AcceptGrantDto = exports.AcceptAgencyMemberInviteDto = exports.RedeemAgencyInviteDto = exports.InviteAgencyMemberDto = exports.CreateAgencyOrganizationDto = exports.AGENCY_ROSTER_METRICS_STATES = exports.AGENCY_ROSTER_NEEDS_ATTENTION_STATES = exports.AGENCY_ROSTER_ROW_STATES = exports.AGENCY_REQUEST_CODES = exports.CREATOR_ACCESS_GRANT_STATES = exports.AGENCY_ORGANIZATION_STATUSES = exports.isAgencyGrantScope = exports.AGENCY_GRANT_SCOPES = exports.AGENCY_ROUTE_PATHS = void 0;
|
|
13
13
|
const class_validator_1 = require("class-validator");
|
|
14
14
|
// ── Cross-repo route constants ───────────────────────────────────────────────
|
|
15
15
|
/**
|
|
@@ -89,6 +89,16 @@ exports.CREATOR_ACCESS_GRANT_STATES = [
|
|
|
89
89
|
'expired',
|
|
90
90
|
'revoked',
|
|
91
91
|
];
|
|
92
|
+
/**
|
|
93
|
+
* Outcomes of discovery and requests that are not refusals of access, sent as
|
|
94
|
+
* codes so the screen composes the sentence in the reader's language (FR-019,
|
|
95
|
+
* review M4). A 429 carries `{ code, limit, windowMinutes }`.
|
|
96
|
+
*/
|
|
97
|
+
exports.AGENCY_REQUEST_CODES = {
|
|
98
|
+
RATE_LIMITED: 'agency-rate-limited',
|
|
99
|
+
REQUEST_EXPIRED: 'agency-request-expired',
|
|
100
|
+
REQUEST_ANSWERED: 'agency-request-answered',
|
|
101
|
+
};
|
|
92
102
|
// ── Roster ───────────────────────────────────────────────────────────────────
|
|
93
103
|
/**
|
|
94
104
|
* Roster row states, DECLARED IN PRECEDENCE ORDER (data-model §Derived).
|
|
@@ -104,6 +114,13 @@ exports.AGENCY_ROSTER_ROW_STATES = [
|
|
|
104
114
|
'lapsed',
|
|
105
115
|
/** the creator has no live platform connection */
|
|
106
116
|
'disconnected',
|
|
117
|
+
/**
|
|
118
|
+
* shared within the last 24 hours and not synced yet: the first daily sync
|
|
119
|
+
* has not run. Not stale, and not something the agency can act on (T067,
|
|
120
|
+
* the 2026-09-26 UI run: a creator who agreed a minute ago read as
|
|
121
|
+
* "needs attention").
|
|
122
|
+
*/
|
|
123
|
+
'syncing',
|
|
107
124
|
/** no successful daily sync in 24 hours (FR-011, Jan 2026-09-21) */
|
|
108
125
|
'stale',
|
|
109
126
|
'active',
|
|
@@ -120,6 +137,22 @@ exports.AGENCY_ROSTER_NEEDS_ATTENTION_STATES = [
|
|
|
120
137
|
'disconnected',
|
|
121
138
|
'stale',
|
|
122
139
|
];
|
|
140
|
+
/**
|
|
141
|
+
* The rows that carry numbers — ONE home for "which creators' numbers may the
|
|
142
|
+
* agency see on the roster" (decided 2026-09-24, review M1/M2).
|
|
143
|
+
*
|
|
144
|
+
* Only a row whose card OPENS carries `metrics` and counts in the followers,
|
|
145
|
+
* followers-delta and comments totals: `active`, and `stale` (the card opens;
|
|
146
|
+
* the numbers are as old as `lastSyncAt` says). A `lapsed` creator's reads are
|
|
147
|
+
* refused on the card, so their numbers must not reach the agency through the
|
|
148
|
+
* roster instead; a `disconnected` creator has nothing live to count; a
|
|
149
|
+
* `requested` creator has shared nothing yet.
|
|
150
|
+
*/
|
|
151
|
+
exports.AGENCY_ROSTER_METRICS_STATES = [
|
|
152
|
+
'syncing',
|
|
153
|
+
'stale',
|
|
154
|
+
'active',
|
|
155
|
+
];
|
|
123
156
|
// ── DTOs ─────────────────────────────────────────────────────────────────────
|
|
124
157
|
/**
|
|
125
158
|
* Every DTO below carries class-validator decorators and a constructor that
|
|
@@ -272,7 +305,8 @@ class AgencyAccessRequestDto {
|
|
|
272
305
|
}
|
|
273
306
|
exports.AgencyAccessRequestDto = AgencyAccessRequestDto;
|
|
274
307
|
__decorate([
|
|
275
|
-
(0, class_validator_1.
|
|
308
|
+
(0, class_validator_1.IsString)(),
|
|
309
|
+
(0, class_validator_1.Matches)(/^[A-Za-z0-9-]{1,128}$/),
|
|
276
310
|
__metadata("design:type", String)
|
|
277
311
|
], AgencyAccessRequestDto.prototype, "userId", void 0);
|
|
278
312
|
__decorate([
|
|
@@ -281,6 +315,22 @@ __decorate([
|
|
|
281
315
|
(0, class_validator_1.IsIn)(exports.AGENCY_GRANT_SCOPES, { each: true }),
|
|
282
316
|
__metadata("design:type", Array)
|
|
283
317
|
], AgencyAccessRequestDto.prototype, "scopes", void 0);
|
|
318
|
+
/** `PATCH /users/me/agency-discoverable` — "Let agencies find me" (FR-015). */
|
|
319
|
+
class UpdateAgencyDiscoverableDto {
|
|
320
|
+
/**
|
|
321
|
+
* NO default: a required boolean that defaults to false lets an empty body
|
|
322
|
+
* through validation and reach the database as `undefined` (review M7).
|
|
323
|
+
*/
|
|
324
|
+
constructor(data) {
|
|
325
|
+
if (data?.discoverable !== undefined)
|
|
326
|
+
this.discoverable = data.discoverable;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
exports.UpdateAgencyDiscoverableDto = UpdateAgencyDiscoverableDto;
|
|
330
|
+
__decorate([
|
|
331
|
+
(0, class_validator_1.IsBoolean)(),
|
|
332
|
+
__metadata("design:type", Boolean)
|
|
333
|
+
], UpdateAgencyDiscoverableDto.prototype, "discoverable", void 0);
|
|
284
334
|
class AdminSetOrganizationStatusDto {
|
|
285
335
|
constructor(data) {
|
|
286
336
|
this.status = data?.status ?? 'approved';
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* Covers subscription tier state, Stripe checkout flows,
|
|
5
5
|
* and the customer portal redirect.
|
|
6
6
|
*/
|
|
7
|
+
import type { I_SponsoredBy, I_SponsorshipEnded } from '../agency';
|
|
7
8
|
export type SchedulingSubscriptionTier = 'free' | 'starter' | 'pro';
|
|
8
9
|
export type SchedulingSubscriptionStatus = 'active' | 'canceled' | 'expired';
|
|
9
10
|
/**
|
|
10
11
|
* Constructor input. Takes `maxAccounts`, or the deprecated `maxPlatforms` so
|
|
11
12
|
* code written against 1.40.0 keeps compiling; either way both fields are set.
|
|
12
13
|
*/
|
|
13
|
-
export type SchedulingSubscriptionInfoInput = Pick<SchedulingSubscriptionInfoDTO, 'tier' | 'status' | 'expiresAt'> & ({
|
|
14
|
+
export type SchedulingSubscriptionInfoInput = Pick<SchedulingSubscriptionInfoDTO, 'tier' | 'status' | 'expiresAt'> & Partial<Pick<SchedulingSubscriptionInfoDTO, 'sponsoredBy' | 'sponsorshipEnded'>> & ({
|
|
14
15
|
maxAccounts: number;
|
|
15
16
|
maxPlatforms?: number;
|
|
16
17
|
} | {
|
|
@@ -32,6 +33,14 @@ export declare class SchedulingSubscriptionInfoDTO {
|
|
|
32
33
|
* Read `maxAccounts`. Always equal to it; removed in a later release.
|
|
33
34
|
*/
|
|
34
35
|
maxPlatforms: number;
|
|
36
|
+
/**
|
|
37
|
+
* Present only while an agency covers this creator's plan (spec 175 FR-014).
|
|
38
|
+
* `until` appears once the sponsorship is ending: the date the plan runs to.
|
|
39
|
+
* Absent — never null — for everyone else, so "not sponsored" has one shape.
|
|
40
|
+
*/
|
|
41
|
+
sponsoredBy?: I_SponsoredBy;
|
|
42
|
+
/** Present only when a coverage ended recently and nothing replaced it. Absent otherwise. */
|
|
43
|
+
sponsorshipEnded?: I_SponsorshipEnded;
|
|
35
44
|
constructor(data: SchedulingSubscriptionInfoInput);
|
|
36
45
|
}
|
|
37
46
|
/**
|
|
@@ -19,6 +19,10 @@ class SchedulingSubscriptionInfoDTO {
|
|
|
19
19
|
this.expiresAt = data.expiresAt;
|
|
20
20
|
this.maxAccounts = maxAccounts;
|
|
21
21
|
this.maxPlatforms = maxAccounts;
|
|
22
|
+
if (data.sponsoredBy)
|
|
23
|
+
this.sponsoredBy = data.sponsoredBy;
|
|
24
|
+
if (data.sponsorshipEnded)
|
|
25
|
+
this.sponsorshipEnded = data.sponsorshipEnded;
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
exports.SchedulingSubscriptionInfoDTO = SchedulingSubscriptionInfoDTO;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ad2app-lib",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.47.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"prepare": "npm run build"
|
|
52
52
|
},
|
|
53
53
|
"keywords": [],
|
|
54
|
-
"author": "Maciej
|
|
54
|
+
"author": "Maciej Górski@ad2.app",
|
|
55
55
|
"license": "ISC",
|
|
56
56
|
"description": "Package to share types and utils across the ad2app projects",
|
|
57
57
|
"dependencies": {
|
|
@@ -82,6 +82,25 @@ export const ACCESS_DENIAL_CODES = {
|
|
|
82
82
|
* per person, FR-004). Names the case and NOTHING else about that account.
|
|
83
83
|
*/
|
|
84
84
|
AGENCY_ALREADY_IN_ORGANIZATION: "agency-already-in-organization",
|
|
85
|
+
/**
|
|
86
|
+
* The owner invited an address that is ALREADY on this organization's team,
|
|
87
|
+
* themselves included (T063). A different case from the one above: the owner
|
|
88
|
+
* is told this person is already in, not that they belong elsewhere.
|
|
89
|
+
*/
|
|
90
|
+
AGENCY_ALREADY_MEMBER: "agency-already-member",
|
|
91
|
+
/**
|
|
92
|
+
* The owner tried to cover a creator who pays for their own plan. Covering
|
|
93
|
+
* is for a creator without a paid plan (US4 scenario 3); covering a payer
|
|
94
|
+
* would leave them paying for what the agency also pays for (T066).
|
|
95
|
+
*/
|
|
96
|
+
AGENCY_CREATOR_PAYS_OWN_PLAN: "agency-creator-pays-own-plan",
|
|
97
|
+
/**
|
|
98
|
+
* A creator whose plan an agency covers asked for the billing portal, and
|
|
99
|
+
* has no Stripe customer of their own to open it for (FR-014). The body
|
|
100
|
+
* names the covering agency; the client hides the portal button instead of
|
|
101
|
+
* showing a Stripe error.
|
|
102
|
+
*/
|
|
103
|
+
SPONSORED_NO_PORTAL: "sponsored-no-portal",
|
|
85
104
|
} as const;
|
|
86
105
|
|
|
87
106
|
export type AccessDenialCode =
|
|
@@ -87,7 +87,7 @@ test('every AGENCY code is namespaced, so a new domain cannot shadow an old wall
|
|
|
87
87
|
key.startsWith('AGENCY_'),
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
-
assert.equal(agencyCodes.length,
|
|
90
|
+
assert.equal(agencyCodes.length, 14);
|
|
91
91
|
for (const [, value] of agencyCodes) {
|
|
92
92
|
assert.match(value, /^agency-/, `un-namespaced agency code: ${value}`);
|
|
93
93
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { ACCESS_DENIAL_CODES } from './I_AccessDenial';
|
|
5
|
+
import type { I_AgencyBilling, I_SponsoredBy } from './agency';
|
|
6
|
+
import { SchedulingSubscriptionInfoDTO } from './scheduling/I_SchedulingProUpgrade';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Spec 175 US4 (T022/T024). The billing contract between backend and frontend:
|
|
10
|
+
* the settings screen, the creator's own subscription screen, and the one new
|
|
11
|
+
* refusal a sponsored creator can meet.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
test('a sponsored creator without a Stripe customer is refused the portal with its own code', () => {
|
|
15
|
+
assert.equal(ACCESS_DENIAL_CODES.SPONSORED_NO_PORTAL, 'sponsored-no-portal');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('the subscription DTO carries sponsoredBy when the agency covers the plan', () => {
|
|
19
|
+
const sponsoredBy: I_SponsoredBy = { organizationName: 'Northlight', tier: 'pro', until: '2026-10-04T00:00:00.000Z' };
|
|
20
|
+
const dto = new SchedulingSubscriptionInfoDTO({
|
|
21
|
+
tier: 'pro',
|
|
22
|
+
status: 'active',
|
|
23
|
+
expiresAt: null,
|
|
24
|
+
maxAccounts: 10,
|
|
25
|
+
sponsoredBy,
|
|
26
|
+
});
|
|
27
|
+
assert.deepEqual(dto.sponsoredBy, sponsoredBy);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('the subscription DTO leaves sponsoredBy ABSENT for everyone else, never null', () => {
|
|
31
|
+
const dto = new SchedulingSubscriptionInfoDTO({ tier: 'free', status: 'expired', expiresAt: null, maxAccounts: 0 });
|
|
32
|
+
assert.equal('sponsoredBy' in dto, false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('the billing overview names seats, price, invoices, who is covered and who left', () => {
|
|
36
|
+
const billing: I_AgencyBilling = {
|
|
37
|
+
subscriptionStatus: 'active',
|
|
38
|
+
seats: 3,
|
|
39
|
+
billedSeats: 3,
|
|
40
|
+
price: { amount: 2500, currency: 'USD', interval: 'month' },
|
|
41
|
+
currentPeriodEnd: '2026-10-04T00:00:00.000Z',
|
|
42
|
+
cancelAtPeriodEnd: false,
|
|
43
|
+
nextInvoice: { amount: 7500, currency: 'USD', date: '2026-10-04T00:00:00.000Z' },
|
|
44
|
+
discount: { name: 'First agency', percentOff: 100 },
|
|
45
|
+
paymentMethod: { brand: 'visa', last4: '4242', expMonth: 9, expYear: 2029 },
|
|
46
|
+
billingDetails: { name: 'Northlight Talent', email: 'kontakt@northlight.pl', country: 'PL' },
|
|
47
|
+
invoices: [
|
|
48
|
+
{ id: 'in_1', number: 'AD2-0001', date: '2026-09-04T00:00:00.000Z', amount: 7500, currency: 'USD', status: 'paid' },
|
|
49
|
+
],
|
|
50
|
+
covered: [
|
|
51
|
+
{ grantId: 'g1', creator: { id: 'u1', handle: 'marta.moves' }, tier: 'pro', since: '2026-09-10T00:00:00.000Z' },
|
|
52
|
+
{ grantId: 'g2', creator: { id: 'u2', handle: 'kubaplays' }, tier: null },
|
|
53
|
+
],
|
|
54
|
+
pastCreators: [
|
|
55
|
+
{
|
|
56
|
+
grantId: 'g3',
|
|
57
|
+
creator: { id: null, handle: 'bartek.runs' },
|
|
58
|
+
endedAt: '2026-09-20T00:00:00.000Z',
|
|
59
|
+
endedBy: 'creator',
|
|
60
|
+
coveredUntil: '2026-10-04T00:00:00.000Z',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
assert.equal(billing.covered.filter((row) => row.tier !== null).length, 1);
|
|
65
|
+
assert.equal(billing.pastCreators[0].creator.id, null);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('billedSeats is what Stripe bills: never below one, while seats is the creators with an active grant (review M1)', () => {
|
|
69
|
+
const billing: I_AgencyBilling = {
|
|
70
|
+
subscriptionStatus: 'none', seats: 0, billedSeats: 1, price: null, invoices: [], covered: [], pastCreators: [],
|
|
71
|
+
};
|
|
72
|
+
assert.equal(billing.billedSeats, 1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('the subscription DTO carries a RECENTLY ENDED coverage so the creator is told why the paywall is back (review M4, FR-014)', () => {
|
|
76
|
+
const dto = new SchedulingSubscriptionInfoDTO({
|
|
77
|
+
tier: 'free', status: 'expired', expiresAt: null, maxAccounts: 0,
|
|
78
|
+
sponsorshipEnded: { organizationName: 'Northlight', endedAt: '2026-10-04T00:00:00.000Z' },
|
|
79
|
+
});
|
|
80
|
+
assert.deepEqual(dto.sponsorshipEnded, { organizationName: 'Northlight', endedAt: '2026-10-04T00:00:00.000Z' });
|
|
81
|
+
const plain = new SchedulingSubscriptionInfoDTO({ tier: 'free', status: 'expired', expiresAt: null, maxAccounts: 0 });
|
|
82
|
+
assert.equal('sponsorshipEnded' in plain, false);
|
|
83
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { strict as assert } from 'node:assert';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { validateSync } from 'class-validator';
|
|
4
|
+
|
|
5
|
+
import { AgencyAccessRequestDto, UpdateAgencyDiscoverableDto, type I_AgencyDiscoverResult, type I_AgencyRosterRow, type I_ConnectedPlatforms } from './agency';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Spec 175 US5 (T026/T028): the discover and request contract.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
test('an access request accepts a Firebase uid, not only a UUID: most creators have one', () => {
|
|
12
|
+
const dto = new AgencyAccessRequestDto({ userId: 'fb59705bBaba4f2cA569B54ccbed', scopes: ['analytics'] });
|
|
13
|
+
assert.deepEqual(validateSync(dto), []);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('an access request refuses an id that could not be a user id, and an empty scope set', () => {
|
|
17
|
+
assert.notDeepEqual(validateSync(new AgencyAccessRequestDto({ userId: 'nul\u0000byte', scopes: ['analytics'] })), []);
|
|
18
|
+
assert.notDeepEqual(validateSync(new AgencyAccessRequestDto({ userId: 'abc', scopes: [] })), []);
|
|
19
|
+
assert.notDeepEqual(validateSync(new AgencyAccessRequestDto({ userId: 'abc', scopes: ['dms' as never] })), []);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('the discoverable switch is a boolean and nothing else', () => {
|
|
23
|
+
assert.deepEqual(validateSync(new UpdateAgencyDiscoverableDto({ discoverable: true })), []);
|
|
24
|
+
assert.notDeepEqual(validateSync(Object.assign(new UpdateAgencyDiscoverableDto(), { discoverable: 'yes' })), []);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('a discover answer is ONE shape: hits carry handle, platforms and the platform matched only; a miss is an empty list (FR-015, T068)', () => {
|
|
28
|
+
// Two different creators can hold the same username on different platforms
|
|
29
|
+
// (@mark on YouTube, another @mark on Instagram): every match is returned,
|
|
30
|
+
// each naming where it matched, as separate people (Maciej, 2026-09-26).
|
|
31
|
+
const hits: I_AgencyDiscoverResult = {
|
|
32
|
+
results: [
|
|
33
|
+
{ userId: 'u1', handle: 'mark', platforms: ['youtube'], matchedPlatform: 'youtube' },
|
|
34
|
+
{ userId: 'u2', handle: 'mark', platforms: ['instagram', 'tiktok'], matchedPlatform: 'instagram' },
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
const byEmail: I_AgencyDiscoverResult = { results: [{ userId: 'u3', handle: 'kasia.cooks', platforms: ['instagram'] }] };
|
|
38
|
+
const miss: I_AgencyDiscoverResult = { results: [] };
|
|
39
|
+
assert.deepEqual(Object.keys(hits.results[0]).sort(), ['handle', 'matchedPlatform', 'platforms', 'userId']);
|
|
40
|
+
assert.equal(byEmail.results[0].matchedPlatform, undefined);
|
|
41
|
+
assert.deepEqual(miss.results, []);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
import { AGENCY_REQUEST_CODES, type I_CreatorAccessGrant } from './agency';
|
|
45
|
+
|
|
46
|
+
test('the discoverable switch refuses a body with no value, rather than defaulting it (review M7)', () => {
|
|
47
|
+
const dto = Object.assign(new UpdateAgencyDiscoverableDto(), {});
|
|
48
|
+
assert.notDeepEqual(validateSync(dto), []);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('request and discovery outcomes travel as codes, so the screen composes EN and PL itself (review M4)', () => {
|
|
52
|
+
assert.deepEqual(AGENCY_REQUEST_CODES, {
|
|
53
|
+
RATE_LIMITED: 'agency-rate-limited',
|
|
54
|
+
REQUEST_EXPIRED: 'agency-request-expired',
|
|
55
|
+
REQUEST_ANSWERED: 'agency-request-answered',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('a grant carries the agency’s open second ask separately from what the creator granted (review H4, M3)', () => {
|
|
60
|
+
const grant: Pick<I_CreatorAccessGrant, 'grantedScopes' | 'reaskedScopes' | 'reaskExpiresAt'> = {
|
|
61
|
+
grantedScopes: ['analytics'],
|
|
62
|
+
reaskedScopes: ['comments'],
|
|
63
|
+
reaskExpiresAt: '2026-10-09T00:00:00.000Z',
|
|
64
|
+
};
|
|
65
|
+
assert.deepEqual(grant.reaskedScopes, ['comments']);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a roster row can carry the agency\'s last second ask and when another is possible, and nothing about the answer', () => {
|
|
69
|
+
const row: I_AgencyRosterRow = {
|
|
70
|
+
grantId: 'g1', creator: { id: 'c1', handle: 'h' }, platforms: ['instagram'], state: 'active', grantedScopes: ['analytics'],
|
|
71
|
+
reaskedAt: '2026-09-25T19:43:02.959Z', reaskAvailableAt: '2026-10-09T19:43:02.959Z',
|
|
72
|
+
};
|
|
73
|
+
assert.deepEqual(Object.keys(row).filter((k) => k.startsWith('reask')).sort(), ['reaskAvailableAt', 'reaskedAt']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('connected platforms are a plain list in the lib\'s spelling', () => {
|
|
77
|
+
const answer: I_ConnectedPlatforms = { platforms: ['instagram', 'tiktok'] };
|
|
78
|
+
assert.equal(answer.platforms.length, 2);
|
|
79
|
+
});
|
package/src/types/agency.test.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CREATOR_ACCESS_GRANT_STATES,
|
|
8
8
|
AGENCY_ROSTER_ROW_STATES,
|
|
9
9
|
AGENCY_ROSTER_NEEDS_ATTENTION_STATES,
|
|
10
|
+
AGENCY_ROSTER_METRICS_STATES,
|
|
10
11
|
isAgencyGrantScope,
|
|
11
12
|
CreateAgencyOrganizationDto,
|
|
12
13
|
AgencyAccessRequestDto,
|
|
@@ -50,7 +51,7 @@ test("grant states are the five the data model pins, including declined and expi
|
|
|
50
51
|
test("roster row states include `stale`, and `revoked` is NOT one of them", () => {
|
|
51
52
|
assert.deepEqual(
|
|
52
53
|
[...AGENCY_ROSTER_ROW_STATES],
|
|
53
|
-
["requested", "lapsed", "disconnected", "stale", "active"],
|
|
54
|
+
["requested", "lapsed", "disconnected", "syncing", "stale", "active"],
|
|
54
55
|
);
|
|
55
56
|
assert.equal(
|
|
56
57
|
(AGENCY_ROSTER_ROW_STATES as readonly string[]).includes("revoked"),
|
|
@@ -66,6 +67,11 @@ test("needs-attention is disconnected + lapsed + stale, and never `requested` (F
|
|
|
66
67
|
false,
|
|
67
68
|
"a requested grant waits on the CREATOR, so it never lights the agency's badge",
|
|
68
69
|
);
|
|
70
|
+
assert.equal(
|
|
71
|
+
(AGENCY_ROSTER_NEEDS_ATTENTION_STATES as readonly string[]).includes("syncing"),
|
|
72
|
+
false,
|
|
73
|
+
"a creator who joined within the day waits on the first sync, not on the agency (T067)",
|
|
74
|
+
);
|
|
69
75
|
// Every needs-attention state must be a real roster state, or the tile counts a
|
|
70
76
|
// state no row can ever hold.
|
|
71
77
|
for (const state of AGENCY_ROSTER_NEEDS_ATTENTION_STATES) {
|
|
@@ -73,6 +79,20 @@ test("needs-attention is disconnected + lapsed + stale, and never `requested` (F
|
|
|
73
79
|
}
|
|
74
80
|
});
|
|
75
81
|
|
|
82
|
+
test("only rows whose card opens carry numbers: active and stale (review M1/M2, 2026-09-24)", () => {
|
|
83
|
+
assert.deepEqual([...AGENCY_ROSTER_METRICS_STATES], ["syncing", "stale", "active"]);
|
|
84
|
+
for (const blocked of ["requested", "lapsed", "disconnected"]) {
|
|
85
|
+
assert.equal(
|
|
86
|
+
(AGENCY_ROSTER_METRICS_STATES as readonly string[]).includes(blocked),
|
|
87
|
+
false,
|
|
88
|
+
`a ${blocked} row must carry no metrics and count in no total`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
for (const state of AGENCY_ROSTER_METRICS_STATES) {
|
|
92
|
+
assert.ok((AGENCY_ROSTER_ROW_STATES as readonly string[]).includes(state));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
76
96
|
test("DTOs construct from a plain object (the house `new I_DTO()` shape)", () => {
|
|
77
97
|
const org = new CreateAgencyOrganizationDto({
|
|
78
98
|
name: "Studio Kot",
|
package/src/types/agency.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrayNotEmpty,
|
|
3
3
|
IsArray,
|
|
4
|
+
IsBoolean,
|
|
4
5
|
IsEmail,
|
|
5
6
|
IsIn,
|
|
6
7
|
IsNotEmpty,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
IsUrl,
|
|
10
11
|
IsUUID,
|
|
11
12
|
Length,
|
|
13
|
+
Matches,
|
|
12
14
|
} from 'class-validator';
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -145,6 +147,12 @@ export interface I_AgencyMember {
|
|
|
145
147
|
/** Null while an e-mail invitation is outstanding — nobody has accepted yet. */
|
|
146
148
|
userId?: string;
|
|
147
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;
|
|
148
156
|
role: AgencyMemberRole;
|
|
149
157
|
invitedAt: string;
|
|
150
158
|
/** "Who is in" is exactly `acceptedAt != null`. */
|
|
@@ -216,8 +224,30 @@ export interface I_CreatorAccessGrant {
|
|
|
216
224
|
sponsorship?: { tier: SponsoredTier; since: string; until?: string };
|
|
217
225
|
/** Which platforms the grant reaches right now — derived at read time, never stored. */
|
|
218
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;
|
|
219
236
|
}
|
|
220
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
|
+
|
|
221
251
|
// ── Roster ───────────────────────────────────────────────────────────────────
|
|
222
252
|
|
|
223
253
|
/**
|
|
@@ -234,6 +264,13 @@ export const AGENCY_ROSTER_ROW_STATES = [
|
|
|
234
264
|
'lapsed',
|
|
235
265
|
/** the creator has no live platform connection */
|
|
236
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',
|
|
237
274
|
/** no successful daily sync in 24 hours (FR-011, Jan 2026-09-21) */
|
|
238
275
|
'stale',
|
|
239
276
|
'active',
|
|
@@ -257,30 +294,115 @@ export const AGENCY_ROSTER_NEEDS_ATTENTION_STATES = [
|
|
|
257
294
|
export type AgencyRosterNeedsAttentionState =
|
|
258
295
|
(typeof AGENCY_ROSTER_NEEDS_ATTENTION_STATES)[number];
|
|
259
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
|
+
|
|
260
330
|
export interface I_AgencyRosterRow {
|
|
261
331
|
grantId: string;
|
|
262
332
|
creator: { id: string; handle: string };
|
|
263
333
|
platforms: PublishPlatform[];
|
|
264
334
|
state: AgencyRosterRowState;
|
|
265
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
|
+
*/
|
|
266
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[];
|
|
267
367
|
}
|
|
268
368
|
|
|
269
369
|
export interface I_AgencyRoster {
|
|
270
370
|
totals: {
|
|
271
371
|
creators: number;
|
|
272
372
|
active: number;
|
|
273
|
-
/**
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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;
|
|
278
391
|
/** Count of rows in AGENCY_ROSTER_NEEDS_ATTENTION_STATES. */
|
|
279
392
|
needsAttention: number;
|
|
280
393
|
};
|
|
281
394
|
items: I_AgencyRosterRow[];
|
|
282
395
|
}
|
|
283
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
|
+
|
|
284
406
|
/** What a sponsored creator sees on their OWN subscription screen (FR-014). */
|
|
285
407
|
export interface I_SponsoredBy {
|
|
286
408
|
organizationName: string;
|
|
@@ -289,6 +411,89 @@ export interface I_SponsoredBy {
|
|
|
289
411
|
until?: string;
|
|
290
412
|
}
|
|
291
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
|
+
|
|
292
497
|
// ── DTOs ─────────────────────────────────────────────────────────────────────
|
|
293
498
|
|
|
294
499
|
/**
|
|
@@ -426,7 +631,13 @@ export class AgencyDiscoverQueryDto {
|
|
|
426
631
|
}
|
|
427
632
|
|
|
428
633
|
export class AgencyAccessRequestDto {
|
|
429
|
-
|
|
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}$/)
|
|
430
641
|
userId: string;
|
|
431
642
|
|
|
432
643
|
@IsArray()
|
|
@@ -440,6 +651,48 @@ export class AgencyAccessRequestDto {
|
|
|
440
651
|
}
|
|
441
652
|
}
|
|
442
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
|
+
|
|
443
696
|
export class AdminSetOrganizationStatusDto {
|
|
444
697
|
@IsIn(['approved', 'rejected'])
|
|
445
698
|
status: Extract<AgencyOrganizationStatus, 'approved' | 'rejected'>;
|
|
@@ -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
|
|