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.
@@ -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
- /** Summed only over creators holding `analytics`; disconnected platforms excluded. */
261
- followers: number;
262
- followersDelta7d: number;
263
- /** Calendar week, Mon-Sun, organization timezone, creators holding `comments`. */
264
- commentsThisWeek: number;
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: CreateAgencyOrganizationDto) {
289
- this.name = data.name;
290
- this.website = data.website;
291
- this.registryId = data.registryId;
292
- this.registryCountry = data.registryCountry;
293
- this.contactEmail = data.contactEmail;
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: InviteAgencyMemberDto) {
301
- this.email = data.email;
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: RedeemAgencyInviteDto) {
309
- this.token = data.token;
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: AcceptAgencyMemberInviteDto) {
317
- this.token = data.token;
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: AcceptGrantDto) {
331
- this.token = data.token;
332
- this.grantId = data.grantId;
333
- this.scopes = data.scopes;
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: UpdateGrantScopesDto) {
342
- this.scopes = data.scopes;
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: SponsorCreatorDto) {
350
- this.tier = data.tier;
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: AgencyDiscoverQueryDto) {
359
- this.q = data.q;
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: AgencyAccessRequestDto) {
368
- this.userId = data.userId;
369
- this.scopes = data.scopes;
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
- /** Required on reject — a rejected applicant is shown this (FR-002). */
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: AdminSetOrganizationStatusDto) {
379
- this.status = data.status;
380
- this.reason = data.reason;
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