ad2app-lib 1.22.0 → 1.24.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.
@@ -22,8 +22,25 @@ export const TERMS_SECTIONS: LegalSection[] = [
22
22
  { text: 'Media and content file management.' },
23
23
  { text: 'Optional connection of third-party AI assistants and tools that support the Model Context Protocol (MCP), letting you manage scheduling, posting, and analytics from within a tool of your choice. See Section 13 and our {PRIVACY}.' },
24
24
  ] },
25
- { kind: 'p', text: 'The Service is offered on three plans: **Free** (connecting new social accounts and publishing are not available on this plan; if you previously had a paid plan, your previously-connected accounts, post history, and analytics also become inaccessible while you are on the Free plan \u2014 that data is retained, not deleted, and becomes available again if you resubscribe), **Starter**, and **Pro** (Starter and Pro differ only in how many social accounts you may connect; all other features are identical). Current plan details and pricing are shown in the app and on our pricing page.' },
26
- { kind: 'p', text: 'Not all features described in these Terms are available in every plan. We may modify the Service where necessary for a valid reason including to comply with law, maintain security, reflect changes in the third-party platforms we integrate with, or improve the Service. Where a modification would negatively and more than minorly affect your access to or use of a paid plan, we will notify you on a durable medium (such as email) at least 30 days in advance and, if you are a consumer, you may terminate your subscription free of charge within 30 days of the notice and receive a pro-rata refund of prepaid Fees for the unused period.' },
25
+ { kind: 'p', text: 'The Service is offered on three plans: **Free** (connecting new social accounts and publishing are not available on this plan; if you previously had a paid plan, your previously-connected accounts, post history, and analytics also become inaccessible while you are on the Free plan. That data is retained, not deleted, and becomes available again if you resubscribe), **Starter**, and **Pro** (Starter and Pro differ only in how many social accounts you may connect; all other features are identical). Current plan details and pricing are shown in the app and on our pricing page.' },
26
+ { kind: 'p', text: 'Not all features described in these Terms are available in every plan. We may modify the Service where this is not necessary to keep it conforming with the contract **only for a valid reason stated here**: to comply with law or a decision of a court or authority; to maintain or restore security; to reflect a change in the third-party platforms we integrate with; or to replace a feature with an equivalent or better one. Where a modification negatively and more than minorly affects your access to or use of the Service, we will notify you on a durable medium (such as email) in advance, describing the change and when it takes effect, and, **if you are a consumer, you may terminate the contract free of charge within 30 days of the later of the date the change is made and the date you are informed of it**, with a pro-rata refund of any prepaid Fees for the unused period. This applies whether you are on a paid plan, the Free plan or the private beta.' },
27
+ ],
28
+ },
29
+ {
30
+ id: 's2a',
31
+ title: '2a. Private Beta',
32
+ blocks: [
33
+ { kind: 'p', text: 'We sometimes offer a **private beta**: a limited number of places on a free, fixed-term grant of full access to the Service. If you accept a beta invitation, this section applies to you in addition to the rest of these Terms, and prevails over them where they conflict.' },
34
+ { kind: 'ul', items: [
35
+ { text: 'The beta is **free of charge**. No payment is due, no payment method is collected, and your beta place never converts into a paid plan automatically.' },
36
+ { text: 'The beta runs for a **fixed period**, stated to you before you accept and confirmed in the message we send you when you accept. Access begins when you accept and **ends automatically on the stated end date**. That end date is the agreed duration of the beta, not a change we make to the contract later.' },
37
+ { text: 'When the beta period ends, **your account moves to the Free plan**. Nothing is charged and nothing is deleted. On the Free plan, connecting new social accounts and publishing are unavailable, and the social accounts, post history and analytics from your beta period become inaccessible while you remain on the Free plan. That data is **retained, not deleted**, and becomes available again if you subscribe to a paid plan. You can also ask us to export it at any time, not only when you leave (Section 12).' },
38
+ { text: 'We will remind you before the beta period ends and tell you when it has ended. Neither message is a condition of the end date taking effect: the end date is already part of these Terms.' },
39
+ { text: 'You may leave the beta at any time, for any reason, at no cost, by closing your account (Section 12) or by writing to us at {EMAIL}.' },
40
+ { text: 'Your **statutory right of withdrawal (Section 8a) applies to the beta contract in full**, exactly as it applies to a paid plan. Nothing in this section limits it.' },
41
+ { text: 'Places are limited. We may close beta registration once the places are filled.' },
42
+ { text: 'We will not end your beta place before the stated end date, except where you materially breach these Terms or the law requires it. The 30-day termination for any other reason in Section 12 **does not apply during the beta period**. Otherwise the fixed period would only be fixed for you.' },
43
+ ] },
27
44
  ],
28
45
  },
29
46
  {
@@ -80,12 +97,12 @@ export const TERMS_SECTIONS: LegalSection[] = [
80
97
  { kind: 'subheading', text: 'Content moderation (Art. 14 DSA)' },
81
98
  { kind: 'p', text: 'We may restrict, remove, or disable access to User Content, or suspend or terminate an account, where the content is illegal or breaches these Terms. Moderation decisions are made by human review; we do not use automated tools or algorithmic decision-making to detect or remove content. If you believe a moderation decision was wrong, you may contest it by writing to kontakt@ad2.app; we will review your objection and reply with a reasoned decision.' },
82
99
  { kind: 'subheading', text: 'Statement of reasons (Art. 17 DSA)' },
83
- { kind: 'p', text: 'Where we remove or disable access to your content, suspend or terminate your account, or restrict payments on the ground that content is illegal or breaches these Terms, we will provide you with a clear and specific statement of reasons what we did and its scope, the facts and circumstances relied on, whether automated means were used in taking the decision, the legal or contractual ground, and clear and user-friendly information on how you can contest the decision except for restrictions taken pursuant to a legal order under Art. 9 DSA (which follow that Article\u2019s own notice rules) or where the content is deceptive high-volume commercial content (spam).' },
100
+ { kind: 'p', text: 'Where we remove or disable access to your content, suspend or terminate your account, or restrict payments on the ground that content is illegal or breaches these Terms, we will provide you with a clear and specific statement of reasons: what we did and its scope, the facts and circumstances relied on, whether automated means were used in taking the decision, the legal or contractual ground, and clear and user-friendly information on how you can contest the decision, except for restrictions taken pursuant to a legal order under Art. 9 DSA (which follow that Article\u2019s own notice rules) or where the content is deceptive high-volume commercial content (spam).' },
84
101
  { kind: 'subheading', text: 'Platform non-liability for User Content' },
85
102
  { kind: 'p', text: '**ad2app does not create, edit, endorse, or control User Content.** We act solely as a hosting intermediary within the meaning of Article 6 of the EU Digital Services Act (Regulation (EU) 2022/2065) and are not liable for User Content unless we have actual knowledge of its illegal nature and fail to act expeditiously to remove or disable access to it.' },
86
103
  { kind: 'p', text: 'You are **solely and exclusively responsible** for any content you create, publish, share, schedule, or distribute through the Service, including but not limited to scheduled posts, captions, images, videos, hashtags, and any other material posted on third-party platforms (including the social media platforms you connect, such as Instagram, TikTok, X (Twitter), YouTube, LinkedIn, Facebook, Threads, Pinterest, Reddit, and Bluesky) using ad2app as a tool or workflow, whether that content was composed directly by you, drafted with the help of a connected AI assistant, or scheduled, planned, or tracked through the platform. ad2app assumes no liability for such content.' },
87
104
  { kind: 'p', text: 'In particular, you are responsible for ensuring that all published content complies with: (a) applicable advertising and marketing laws; (b) platform-specific community guidelines and terms of service of any third-party social media platform; (c) intellectual property rights of third parties; and (d) all applicable laws of the jurisdiction(s) where the content is distributed or viewed.' },
88
- { kind: 'subheading', text: 'Digital Services Act Points of Contact (Art. 11 DSA)' },
105
+ { kind: 'subheading', text: 'Digital Services Act: Points of Contact (Art. 11 DSA)' },
89
106
  { kind: 'p', text: 'In accordance with Art. 11 of Regulation (EU) 2022/2065 (Digital Services Act), ad2app designates the following single point of contact for direct communication with Member State authorities, the European Commission, and the European Board for Digital Services: kontakt@ad2.app. Communications may be conducted in Polish or English.' },
90
107
  { kind: 'subheading', text: 'Point of contact for users (Art. 12 DSA)' },
91
108
  { kind: 'p', text: 'You can contact us directly and rapidly at kontakt@ad2.app. This channel is handled by our team and does not rely solely on automated tools.' },
@@ -140,9 +157,18 @@ export const TERMS_SECTIONS: LegalSection[] = [
140
157
  { text: 'Failure to pay Fees may result in suspension of your access to paid features.' },
141
158
  ],
142
159
  },
143
- { kind: 'p', text: '**Consumer right of withdrawal:** if you are acting as a natural person outside your professional or business capacity (consumer), you have a right of withdrawal from a service contract within 14 days under the Polish Consumer Rights Act (Ustawa o prawach konsumenta) and EU Consumer Rights Directive 2011/83/EU. By expressly requesting immediate access to the Service at the point of purchase and acknowledging this notice, you agree that the right of withdrawal is lost upon full performance of the service, in accordance with Article 16(a) of Directive 2011/83/EU (as implemented by art. 38 of the Polish Ustawa o prawach konsumenta).' },
144
- { kind: 'p', text: 'If you asked us to begin providing the Service during the 14-day withdrawal period and then withdraw before the end of the subscription term, you must pay an amount proportionate to the part of the Service already provided up to the moment you inform us of your withdrawal, calculated against the total contract price; we will refund the balance within 14 days using the same means of payment you used, unless you expressly agree otherwise.' },
145
- { kind: 'p', text: 'If you wish to exercise the right of withdrawal before performance is complete, you may use the standard form below or any unambiguous statement:' },
160
+ ],
161
+ },
162
+ {
163
+ id: 's8a',
164
+ title: '8a. Your Right to Withdraw (Consumers)',
165
+ blocks: [
166
+ { kind: 'p', text: 'This section applies to you if you are a **consumer**, meaning a natural person acting outside your trade, business, craft or profession. It also applies to you if you are a sole trader registered in the Polish CEIDG who concludes this contract in connection with your business, where the contract is not of a professional character for you, judged in particular by the subject of your registered activity (art. 7aa of the Polish Ustawa o prawach konsumenta), which covers most solo creators who happen to be registered. It applies to **every** contract you conclude with us, including the **Free plan and the private beta**, whether or not you pay us anything.' },
167
+ { kind: 'p', text: 'You have the right to withdraw from this contract **within 14 days without giving any reason and without incurring any cost**. The withdrawal period expires **14 days from the day the contract is concluded**, which is the day you create your account or accept a new plan or beta invitation.' },
168
+ { kind: 'p', text: 'To exercise the right of withdrawal, you must inform us of your decision by an unequivocal statement, for example a letter sent by post or an email to {EMAIL}. You may use the model withdrawal form below, but this is not obligatory. To meet the withdrawal deadline it is sufficient for you to send your communication concerning your exercise of the right of withdrawal **before** the withdrawal period has expired. If you withdraw electronically, we will send you an acknowledgement of receipt on a durable medium without delay.' },
169
+ { kind: 'p', text: 'If you withdraw from this contract, we will reimburse to you **all payments received from you** without undue delay and in any event **not later than 14 days** from the day on which we are informed of your decision. We will use the same means of payment as you used for the initial transaction unless you have expressly agreed otherwise; in any event you will not incur any fees as a result of the reimbursement. **On the Free plan and during the private beta there is nothing to reimburse, because nothing has been paid.**' },
170
+ { kind: 'p', text: 'You pay nothing for the period before you withdrew. Polish law would let us charge a proportionate amount for what you used, but only if we had separately asked you, before payment, to expressly request that we start the Service inside the withdrawal period. We do not ask for that, so **a withdrawal within 14 days is a full refund, with no deduction for time already used**.' },
171
+ { kind: 'p', text: 'After you withdraw, you must stop using the Service. We will not use content other than personal data that you provided or created while using the Service, except where that content is only usable in connection with the Service, relates only to your activity while using it, or has been combined with other data and cannot reasonably be separated. On your request we will make that content available to you free of charge, without hindrance, within a reasonable time and in a commonly used, machine-readable format.' },
146
172
  {
147
173
  kind: 'box',
148
174
  label: 'Standard Withdrawal Form',
@@ -174,6 +200,7 @@ export const TERMS_SECTIONS: LegalSection[] = [
174
200
  blocks: [
175
201
  { kind: 'p', text: 'To the maximum extent permitted by applicable law, ad2app shall not be liable for any indirect, incidental, special, consequential, or punitive damages, including loss of profits, data, or goodwill, arising from your use of or inability to use the Service.' },
176
202
  { kind: 'p', text: 'In any case, our total aggregate liability to you for any claim arising from these Terms shall not exceed the greater of: (a) the Fees paid by you to ad2app in the 12 months preceding the claim, or (b) EUR 100.' },
203
+ { kind: 'p', text: '**If you are a consumer, the cap above does not apply to you.** Our liability towards a consumer is governed by Polish law without contractual limitation, and nothing in this Section excludes or limits our liability for non-performance or improper performance of the contract towards a consumer. This matters most on the Free plan and in the private beta, where you have paid us nothing and the cap would otherwise be at its harshest.' },
177
204
  { kind: 'p', text: 'Nothing in these Terms limits liability for death or personal injury caused by negligence, fraud, or any other liability that cannot be excluded by Polish or EU law.' },
178
205
  ],
179
206
  },
@@ -182,6 +209,7 @@ export const TERMS_SECTIONS: LegalSection[] = [
182
209
  title: '11. Indemnification',
183
210
  blocks: [
184
211
  { kind: 'p', text: 'You agree to indemnify, defend, and hold harmless ad2app and its officers, directors, employees, and agents from any claims, liabilities, damages, losses, and expenses (including reasonable legal fees) arising out of or relating to: (a) your use of the Service in violation of these Terms; (b) your User Content; or (c) your violation of any third-party rights.' },
212
+ { kind: 'p', text: '**This Section does not apply to you if you are a consumer.** An open-ended obligation to cover our costs has no place in a contract with a consumer, and we do not ask for one.' },
185
213
  ],
186
214
  },
187
215
  {
@@ -193,12 +221,21 @@ export const TERMS_SECTIONS: LegalSection[] = [
193
221
  items: [
194
222
  { text: 'You may close your account at any time from your account settings or by contacting us at {EMAIL}. When you close your account, your connected social accounts are disconnected and their access revoked immediately and irreversibly; your account data is held in a deactivated state for 30 days, during which you can restore the account (you will need to reconnect your social platforms), after which it is permanently deleted. See our {PRIVACY}.' },
195
223
  { text: 'We may suspend or terminate your account immediately if you materially breach these Terms, or with 30 days\' notice for any other reason. Where we suspend or terminate on the ground of illegal content or a breach of these Terms, we will provide a statement of reasons and redress information as described in Section 5.' },
196
- { text: 'On withdrawal or termination, you may request free of charge, without hindrance, and within a reasonable time an export of the content you provided or created through the Service (such as your drafts and uploaded media), other than personal data governed by our {PRIVACY}, in a commonly used, machine-readable format. Send the request to {EMAIL}.' },
197
- { text: 'Upon termination, your licence to use the Service ceases. Sections 5, 6, 10, 11, 13, 14, and 15 survive termination.' },
224
+ { text: 'At any time, and in particular on withdrawal or termination, you may request, free of charge, without hindrance, and within a reasonable time, an export of the content you provided or created through the Service (such as your drafts and uploaded media), other than personal data governed by our {PRIVACY}, in a commonly used, machine-readable format. Send the request to {EMAIL}.' },
225
+ { text: 'Upon termination, your licence to use the Service ceases. Sections 5, 6, 10, 11, 12a, 13, 14, and 15 survive termination. Section 12a matters in particular, because a complaint is usually made after an account has already been closed.' },
198
226
  ],
199
227
  },
200
228
  ],
201
229
  },
230
+ {
231
+ id: 's12a',
232
+ title: '12a. Complaints',
233
+ blocks: [
234
+ { kind: 'p', text: 'If something is wrong with the Service, write to us at {EMAIL} describing the problem and what you would like us to do.' },
235
+ { kind: 'p', text: '**If you are a consumer, we will reply within 14 days of receiving your complaint, on a durable medium. If we do not reply within that period, your complaint is deemed accepted** (art. 7a of the Polish Ustawa o prawach konsumenta).' },
236
+ { kind: 'p', text: 'This is separate from contesting a content moderation decision (Section 5) and from out-of-court dispute resolution (Section 14).' },
237
+ ],
238
+ },
202
239
  {
203
240
  id: 's13',
204
241
  title: '13. Data Protection and Third-Party Tools',
@@ -212,17 +249,18 @@ export const TERMS_SECTIONS: LegalSection[] = [
212
249
  id: 's14',
213
250
  title: '14. Governing Law and Dispute Resolution',
214
251
  blocks: [
215
- { kind: 'p', text: 'These Terms are governed by and construed in accordance with the laws of Poland, without regard to its conflict-of-law provisions. EU mandatory consumer protection laws apply where you are a consumer within the EU.' },
216
- { kind: 'p', text: 'Any dispute arising from or in connection with these Terms shall be subject to the exclusive jurisdiction of the courts competent for the registered office of ad2app, unless mandatory law provides otherwise.' },
217
- { kind: 'p', text: 'If you are a consumer resident in the EU, you may seek alternative dispute resolution through a certified ADR body. In Poland, the relevant body is the Inspekcja Handlowa (Trade Inspection Authority). ad2app is willing to participate in ADR proceedings where required by applicable law. Our contact address for dispute resolution purposes is kontakt@ad2.app.' },
252
+ { kind: 'p', text: 'These Terms are governed by the laws of Poland. **If you are a consumer, this choice of law does not deprive you of the protection of any provision of the law of the country where you are habitually resident that cannot be departed from by agreement.** Where the law of your country gives you stronger protection than Polish law on a given point, that stronger protection applies to you on that point. This paragraph reflects Article 6(2) of Regulation (EC) No 593/2008 (Rome I).' },
253
+ { kind: 'p', text: 'If you are a **consumer**, you may bring proceedings against us either before the courts of Poland or before the courts of the country where you are habitually resident, whichever you prefer. **We may bring proceedings against you only before the courts of the country where you are habitually resident.** Nothing in these Terms requires you to litigate anywhere other than your own country, and nothing here obliges you to arbitrate. If you are not a consumer, the courts competent for our registered office in Warsaw have exclusive jurisdiction.' },
254
+ { kind: 'p', text: 'If you are a consumer and we cannot resolve your complaint directly, you can use out-of-court dispute resolution. In Poland, the competent bodies include the **Wojewódzki Inspektorat Inspekcji Handlowej** for your voivodeship and the **Rzecznik Konsumentów** in your district (powiat) or city, both free of charge. The register of authorised Polish ADR entities and how to reach them is published by UOKiK at https://polubowne.uokik.gov.pl. Using out-of-court dispute resolution does not affect your right to go to court. Our contact address for dispute resolution purposes is {EMAIL}.' },
218
255
  ],
219
256
  },
220
257
  {
221
258
  id: 's15',
222
259
  title: '15. Changes to These Terms',
223
260
  blocks: [
224
- { kind: 'p', text: 'We may update these Terms at any time. Where changes are material, we will notify you by email or via a prominent notice within the Service at least 14 days before the changes take effect. Your continued use of the Service after that date constitutes your acceptance of the revised Terms. If you do not agree to the revised Terms, you must stop using the Service and close your account.' },
225
- { kind: 'p', text: 'For Users acting as consumers, material changes that affect your rights will require your explicit re-acceptance before they take effect we will provide a clear accept/decline mechanism in such cases.' },
261
+ { kind: 'p', text: 'We may change these Terms only for a **valid reason stated here**: to comply with a change in law or a decision of a court or authority; to reflect a change in the third-party platforms we integrate with; to address a security or technical necessity; to reflect a new feature or plan we introduce; or to correct an error or make the wording clearer without changing what either of us owes. Any other change requires your agreement.' },
262
+ { kind: 'p', text: 'We will notify you of any change on a **durable medium** (normally email) at least **30 days** before it takes effect, setting out what is changing, why, and when. **If you are a consumer, a change that affects your rights or obligations takes effect for you only if you accept it.** We will present a clear accept or decline choice. **We will not treat your silence, or your continued use of the Service, as acceptance.** If you decline, or do not respond, your contract continues on the current Terms until the end of your current billing period, and you may cancel at any time with a pro-rata refund of prepaid Fees for the unused period.' },
263
+
226
264
  ],
227
265
  },
228
266
  {
@@ -2,7 +2,9 @@ import { test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import {
4
4
  ANALYTICS_SOURCES,
5
+ GATED_METRICS,
5
6
  SchedulingAnalyticsKpiDTO,
7
+ type MetricAvailability,
6
8
  } from './I_SchedulingAnalytics';
7
9
 
8
10
  // AD2-1045 — the analytics partial-failure signal: a KPI response can name
@@ -89,3 +91,58 @@ test('impressionsAvailable/reachAvailable are optional and absent by default (ba
89
91
  assert.equal('impressionsAvailable' in serialized, false);
90
92
  assert.equal('reachAvailable' in serialized, false);
91
93
  });
94
+
95
+ // Spec 111 — per-metric structural availability. The two AD2-1078 flags above
96
+ // only ever described impressions and reach; the capability map the backend
97
+ // now derives covers eight metrics, and it travelled as an undeclared field
98
+ // cast onto the DTO on one side and re-widened on the other until this landed.
99
+
100
+ test('SchedulingAnalyticsKpiDTO carries metricAvailability when provided', () => {
101
+ const kpi = new SchedulingAnalyticsKpiDTO({
102
+ impressions: 10,
103
+ reach: 5,
104
+ engagements: 4,
105
+ views: 20,
106
+ engagementRate: 1.2,
107
+ followerGrowth: 3,
108
+ metricAvailability: { reach: false, saves: true, clicks: false },
109
+ });
110
+ assert.equal(kpi.metricAvailability?.reach, false);
111
+ assert.equal(kpi.metricAvailability?.saves, true);
112
+ assert.equal(kpi.metricAvailability?.clicks, false);
113
+ assert.deepEqual(
114
+ JSON.parse(JSON.stringify(kpi)).metricAvailability,
115
+ { reach: false, saves: true, clicks: false },
116
+ );
117
+ });
118
+
119
+ test('metricAvailability is optional and absent by default — absent means UNKNOWN', () => {
120
+ const kpi = new SchedulingAnalyticsKpiDTO({
121
+ impressions: 10,
122
+ reach: 5,
123
+ engagements: 4,
124
+ views: 20,
125
+ engagementRate: 1.2,
126
+ followerGrowth: 3,
127
+ });
128
+ assert.equal(kpi.metricAvailability, undefined);
129
+ assert.equal('metricAvailability' in JSON.parse(JSON.stringify(kpi)), false);
130
+ });
131
+
132
+ test('GATED_METRICS names every metric the capability map gates, and nothing else', () => {
133
+ // engagements is our own roll-up and followerGrowth is a different surface —
134
+ // neither is platform-reported, so neither may appear here.
135
+ assert.deepEqual([...GATED_METRICS], [
136
+ 'impressions',
137
+ 'reach',
138
+ 'likes',
139
+ 'comments',
140
+ 'shares',
141
+ 'saves',
142
+ 'clicks',
143
+ 'views',
144
+ ]);
145
+ const availability: MetricAvailability = {};
146
+ for (const metric of GATED_METRICS) availability[metric] = true;
147
+ assert.equal(Object.keys(availability).length, 8);
148
+ });
@@ -20,6 +20,38 @@ export const ANALYTICS_SOURCES = [
20
20
 
21
21
  export type AnalyticsSource = (typeof ANALYTICS_SOURCES)[number];
22
22
 
23
+ // ── Per-metric structural availability (spec 111) ─────────────────────────────
24
+
25
+ /**
26
+ * The metrics whose visibility is decided by the connected platform mix rather
27
+ * than by the data itself.
28
+ *
29
+ * Not every metric is here. `engagements` is our own roll-up (likes + comments
30
+ * + shares + saves) and `followerGrowth` comes from the follower-stats surface,
31
+ * so neither is platform-reported and neither is gated.
32
+ */
33
+ export const GATED_METRICS = [
34
+ 'impressions',
35
+ 'reach',
36
+ 'likes',
37
+ 'comments',
38
+ 'shares',
39
+ 'saves',
40
+ 'clicks',
41
+ 'views',
42
+ ] as const;
43
+
44
+ export type GatedMetric = (typeof GATED_METRICS)[number];
45
+
46
+ /**
47
+ * Which gated metrics the current platform mix can actually report.
48
+ *
49
+ * An ABSENT entry means UNKNOWN, never unavailable — the producer omits the map
50
+ * when it could not determine the platform mix, and a consumer must fall
51
+ * through to showing the figure rather than withhold on a guess.
52
+ */
53
+ export type MetricAvailability = Partial<Record<GatedMetric, boolean>>;
54
+
23
55
  // ── SchedulingAnalyticsKpiDTO ─────────────────────────────────────────────────
24
56
 
25
57
  /**
@@ -62,6 +94,16 @@ export class SchedulingAnalyticsKpiDTO {
62
94
  * Absent/true = available.
63
95
  */
64
96
  reachAvailable?: boolean;
97
+ /**
98
+ * Structural availability for every gated metric, from the backend's
99
+ * per-platform capability map (spec 111). Supersedes the two flags above,
100
+ * which stay for consumers that only ever read those two.
101
+ *
102
+ * ABSENT means UNKNOWN, not unavailable: the backend omits this whole map
103
+ * when it could not determine the connected platform mix, and a consumer
104
+ * must show the figure rather than withhold on a guess.
105
+ */
106
+ metricAvailability?: MetricAvailability;
65
107
 
66
108
  constructor(data: SchedulingAnalyticsKpiDTO) {
67
109
  this.impressions = data.impressions;
@@ -82,6 +124,9 @@ export class SchedulingAnalyticsKpiDTO {
82
124
  if (data.reachAvailable !== undefined) {
83
125
  this.reachAvailable = data.reachAvailable;
84
126
  }
127
+ if (data.metricAvailability !== undefined) {
128
+ this.metricAvailability = data.metricAvailability;
129
+ }
85
130
  }
86
131
  }
87
132