@ikatec/digisac-api-sdk 4.11.0 → 4.12.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.
Files changed (49) hide show
  1. package/dist/apis/categories/types.d.ts +1 -0
  2. package/dist/apis/categories/types.d.ts.map +1 -1
  3. package/dist/apis/interactiveMessages/InteractiveMessagesApi.cjs +15 -0
  4. package/dist/apis/interactiveMessages/InteractiveMessagesApi.cjs.map +1 -1
  5. package/dist/apis/interactiveMessages/InteractiveMessagesApi.d.ts +13 -0
  6. package/dist/apis/interactiveMessages/InteractiveMessagesApi.d.ts.map +1 -1
  7. package/dist/apis/interactiveMessages/InteractiveMessagesApi.mjs +15 -0
  8. package/dist/apis/interactiveMessages/InteractiveMessagesApi.mjs.map +1 -1
  9. package/dist/apis/interactiveMessages/types.d.ts +8 -2
  10. package/dist/apis/interactiveMessages/types.d.ts.map +1 -1
  11. package/dist/apis/me/MeApi.cjs +36 -29
  12. package/dist/apis/me/MeApi.cjs.map +1 -1
  13. package/dist/apis/me/MeApi.d.ts +33 -22
  14. package/dist/apis/me/MeApi.d.ts.map +1 -1
  15. package/dist/apis/me/MeApi.mjs +36 -29
  16. package/dist/apis/me/MeApi.mjs.map +1 -1
  17. package/dist/apis/me/types.d.ts +67 -4
  18. package/dist/apis/me/types.d.ts.map +1 -1
  19. package/dist/apis/transcripts/TranscriptsApi.cjs +12 -2
  20. package/dist/apis/transcripts/TranscriptsApi.cjs.map +1 -1
  21. package/dist/apis/transcripts/TranscriptsApi.d.ts +11 -5
  22. package/dist/apis/transcripts/TranscriptsApi.d.ts.map +1 -1
  23. package/dist/apis/transcripts/TranscriptsApi.mjs +12 -2
  24. package/dist/apis/transcripts/TranscriptsApi.mjs.map +1 -1
  25. package/dist/apis/transcripts/types.d.ts +4 -0
  26. package/dist/apis/transcripts/types.d.ts.map +1 -1
  27. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.cjs +52 -0
  28. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.cjs.map +1 -1
  29. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.d.ts +45 -2
  30. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.d.ts.map +1 -1
  31. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.mjs +52 -0
  32. package/dist/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.mjs.map +1 -1
  33. package/dist/apis/whatsappBusinessTemplates/types.d.ts +34 -1
  34. package/dist/apis/whatsappBusinessTemplates/types.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/apis/categories/types.ts +1 -0
  37. package/src/apis/interactiveMessages/InteractiveMessagesApi.test.ts +59 -0
  38. package/src/apis/interactiveMessages/InteractiveMessagesApi.ts +26 -0
  39. package/src/apis/interactiveMessages/types.ts +8 -2
  40. package/src/apis/me/MeApi.test.ts +44 -0
  41. package/src/apis/me/MeApi.ts +83 -28
  42. package/src/apis/me/types.ts +75 -4
  43. package/src/apis/payloads.test-d.ts +36 -2
  44. package/src/apis/transcripts/TranscriptsApi.test.ts +14 -21
  45. package/src/apis/transcripts/TranscriptsApi.ts +15 -6
  46. package/src/apis/transcripts/types.ts +4 -0
  47. package/src/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.test.ts +86 -0
  48. package/src/apis/whatsappBusinessTemplates/WhatsappBusinessTemplatesApi.ts +75 -1
  49. package/src/apis/whatsappBusinessTemplates/types.ts +38 -1
@@ -169,7 +169,8 @@ type InteractiveMessageFilePayload =
169
169
  export type InteractiveMessage = {
170
170
  id: string
171
171
  name: string
172
- type: 'list' | 'button' | 'cta_url' | 'request_contact_info'
172
+ /** @deprecated Not a field of the backend (no such column): the type is `interactive.type`. */
173
+ type?: 'list' | 'button' | 'cta_url' | 'request_contact_info'
173
174
  interactive: InteractiveMessageContent
174
175
  archivedAt: string | null
175
176
  accountId: string
@@ -179,9 +180,10 @@ export type InteractiveMessage = {
179
180
  // relationships
180
181
  file?: File | null
181
182
  departments?: Department[]
183
+ account?: Record<string, unknown>
182
184
  }
183
185
 
184
- export type InteractiveMessageRelationships = 'file' | 'departments'
186
+ export type InteractiveMessageRelationships = 'file' | 'departments' | 'account'
185
187
 
186
188
  export type CreateInteractiveMessagePayload = {
187
189
  name: string
@@ -191,6 +193,10 @@ export type CreateInteractiveMessagePayload = {
191
193
  file?: InteractiveMessageFilePayload
192
194
  }
193
195
 
196
+ /**
197
+ * The backend requires `name` and `interactive` on the update too. It replaces the departments: omitting
198
+ * `departmentIds`/`departments` clears them. Only `file.base64Url` is read (a `file.id` is ignored).
199
+ */
194
200
  export type UpdateInteractiveMessagePayload = {
195
201
  name?: string
196
202
  interactive?: InteractiveMessageContentInput
@@ -110,6 +110,50 @@ describe('MeApi', () => {
110
110
  })
111
111
  })
112
112
 
113
+ describe('getMe with a query', () => {
114
+ it('serializes the include and keeps the headers', async () => {
115
+ vi.mocked(client.get).mockResolvedValue(user)
116
+ const headers = { 'X-Custom': 'h' }
117
+ await api.getMe({ include: ['roles', 'account'] }, headers)
118
+ const [url, sent] = vi.mocked(client.get).mock.calls[0] ?? []
119
+ expect(url).toMatch(/^\/me\?/)
120
+ expect(url).toContain('include')
121
+ expect(sent).toBe(headers)
122
+ })
123
+ })
124
+
125
+ describe('getAccount with where.id', () => {
126
+ it('serializes the id of the account', async () => {
127
+ vi.mocked(client.get).mockResolvedValue(account)
128
+ await api.getAccount({ where: { id: 'acc-1' } })
129
+ const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
130
+ expect(url).toMatch(/^\/me\/account\?/)
131
+ expect(url).toContain('acc-1')
132
+ })
133
+ })
134
+
135
+ describe('getAccountAmounts with a filter', () => {
136
+ it('sends the filter as a comma-separated list', async () => {
137
+ vi.mocked(client.get).mockResolvedValue({ accountId: 'acc-1' })
138
+ await api.getAccountAmounts({ filter: ['services.amounts', 'users.amounts'] })
139
+ const url = vi.mocked(client.get).mock.calls[0]?.[0] as string
140
+ expect(url).toMatch(/^\/me\/get-account-amounts\?/)
141
+ expect(decodeURIComponent(url)).toContain('services.amounts,users.amounts')
142
+ })
143
+
144
+ it('keeps the headers-only call', async () => {
145
+ const headers = { 'X-Custom': 'h' }
146
+ await api.getAccountAmounts(headers)
147
+ expect(client.get).toHaveBeenCalledWith('/me/get-account-amounts', headers)
148
+ })
149
+
150
+ it('keeps a header that comes as undefined at runtime as a header', async () => {
151
+ const headers = { Authorization: undefined } as unknown as Record<string, string>
152
+ await api.getAccountAmounts(headers)
153
+ expect(client.get).toHaveBeenCalledWith('/me/get-account-amounts', headers)
154
+ })
155
+ })
156
+
113
157
  // ── getAccount ───────────────────────────────────────────────────
114
158
 
115
159
  describe('getAccount', () => {
@@ -2,6 +2,11 @@ import { BaseApi } from '../../core/BaseApi'
2
2
  import type { Account } from '../accounts/types'
3
3
  import type { User } from '../users/types'
4
4
  import type {
5
+ AccountAmounts,
6
+ AccountAmountsQuery,
7
+ MeAccountQuery,
8
+ MeQuery,
9
+ MeStatusResponse,
5
10
  RegisterTokenPayload,
6
11
  SetOtpSecretKeyPayload,
7
12
  TakeoverPayload,
@@ -10,30 +15,74 @@ import type {
10
15
  UpdateMeStatusPayload,
11
16
  } from './types'
12
17
 
18
+ /**
19
+ * A query object always has an array or an object among its values; the headers only have strings (a header that
20
+ * comes as `undefined` at runtime stays a header). That tells the two apart in the methods that took only the
21
+ * headers before and now take a query first.
22
+ */
23
+ function splitQueryAndHeaders<TQuery extends object>(
24
+ queryOrHeaders: TQuery | Record<string, string> | undefined,
25
+ headers: Record<string, string> | undefined,
26
+ ): [TQuery | undefined, Record<string, string> | undefined] {
27
+ const isQuery =
28
+ !!queryOrHeaders &&
29
+ Object.values(queryOrHeaders).some((value) => typeof value === 'object' && value !== null)
30
+ return isQuery
31
+ ? [queryOrHeaders as TQuery, headers]
32
+ : [undefined, queryOrHeaders as Record<string, string> | undefined]
33
+ }
34
+
13
35
  export class MeApi extends BaseApi {
14
36
  /**
15
- * Returns the currently authenticated user, including their roles, departments, and timetable.
37
+ * Returns the currently authenticated user. Without `include`, the user comes without relations (roles,
38
+ * departments, timetable, account).
16
39
  * @permissions users.view
17
40
  */
18
- getMe(headers?: Record<string, string>): Promise<User> {
19
- return this.client.get<User>('/me', headers)
41
+ getMe(headers?: Record<string, string>): Promise<User>
42
+ getMe(query: MeQuery, headers?: Record<string, string>): Promise<User>
43
+ getMe(
44
+ queryOrHeaders?: MeQuery | Record<string, string>,
45
+ headers?: Record<string, string>,
46
+ ): Promise<User> {
47
+ const [query, sent] = splitQueryAndHeaders<MeQuery>(queryOrHeaders, headers)
48
+ return this.client.get<User>(`/me${this.toQueryString(query)}`, sent)
20
49
  }
21
50
 
22
- /** Returns the account associated with the currently authenticated user. */
23
- getAccount(headers?: Record<string, string>): Promise<Account> {
24
- return this.client.get<Account>('/me/account', headers)
51
+ /**
52
+ * Returns the account of the current user. Send `where.id` with the `accountId` of the user (what the web app
53
+ * does): without it, the route answers 500.
54
+ */
55
+ getAccount(headers?: Record<string, string>): Promise<Account>
56
+ getAccount(query: MeAccountQuery, headers?: Record<string, string>): Promise<Account>
57
+ getAccount(
58
+ queryOrHeaders?: MeAccountQuery | Record<string, string>,
59
+ headers?: Record<string, string>,
60
+ ): Promise<Account> {
61
+ const [query, sent] = splitQueryAndHeaders<MeAccountQuery>(queryOrHeaders, headers)
62
+ return this.client.get<Account>(`/me/account${this.toQueryString(query)}`, sent)
25
63
  }
26
64
 
27
65
  /**
28
- * Returns credit and usage amounts for the current account.
29
- * Excludes Digisac's own internal credit amounts.
66
+ * Returns the amounts of the current account named in `filter` (services and/or users). The credit amounts are
67
+ * never answered here. An account without credits control answers 400.
30
68
  */
31
- getAccountAmounts(headers?: Record<string, string>): Promise<Record<string, unknown>> {
32
- return this.client.get<Record<string, unknown>>('/me/get-account-amounts', headers)
69
+ getAccountAmounts(headers?: Record<string, string>): Promise<AccountAmounts>
70
+ getAccountAmounts(
71
+ query: AccountAmountsQuery,
72
+ headers?: Record<string, string>,
73
+ ): Promise<AccountAmounts>
74
+ getAccountAmounts(
75
+ queryOrHeaders?: AccountAmountsQuery | Record<string, string>,
76
+ headers?: Record<string, string>,
77
+ ): Promise<AccountAmounts> {
78
+ const [query, sent] = splitQueryAndHeaders<AccountAmountsQuery>(queryOrHeaders, headers)
79
+ const queryString = query ? this.toQueryString({ filter: query.filter.join(',') }) : ''
80
+ return this.client.get<AccountAmounts>(`/me/get-account-amounts${queryString}`, sent)
33
81
  }
34
82
 
35
83
  /**
36
- * Returns a URL to the Agnus billing/plan management page for the current account.
84
+ * Returns a URL to the Agnus billing/plan management page for the current account. Calls Agnus (an external
85
+ * service) and, for accounts without credits control, reports the consumption to it first.
37
86
  * @permissions myAccount.myPlan
38
87
  */
39
88
  getAgnusMyPlanUrl(headers?: Record<string, string>): Promise<{ url: string }> {
@@ -41,39 +90,45 @@ export class MeApi extends BaseApi {
41
90
  }
42
91
 
43
92
  /**
44
- * Updates the current user's profile fields such as name, language, and preferences.
45
- * If a new password is provided, it is validated against the last 6 password history entries
46
- * and all existing access tokens for the user are revoked.
93
+ * Updates the current user's profile fields such as name, language, and preferences (`data` and `preferences`
94
+ * replace the stored objects). A new `password` needs `currentPassword` and a recent login, is validated against
95
+ * the last 6 passwords, and revokes the access tokens with an expiration — including the one of this request.
47
96
  */
48
97
  updateMe(body: UpdateMePayload, headers?: Record<string, string>): Promise<User> {
49
98
  return this.client.put<User, UpdateMePayload>('/me', body, headers)
50
99
  }
51
100
 
52
- /** Updates account-level settings such as name, settings object, wizard progress, and default department. */
101
+ /**
102
+ * Updates account-level settings such as name, settings object, wizard progress, and default department. It
103
+ * applies to the whole account; the default department is required while the tickets are enabled.
104
+ */
53
105
  updateAccount(body: UpdateMeAccountPayload, headers?: Record<string, string>): Promise<Account> {
54
106
  return this.client.put<Account, UpdateMeAccountPayload>('/me/account', body, headers)
55
107
  }
56
108
 
57
109
  /**
58
- * Updates the presence status of the current user (`online`, `offline`, or `away`).
110
+ * Updates the presence status of the current user (`online`, `offline`, or `away`) for a `client`.
59
111
  * If setting to `online`, validates the user's timetable first — if outside working hours,
60
- * the status is set to `offline` instead.
112
+ * the status is set to `offline` instead. Resolves the status fields of the user.
61
113
  */
62
- updateStatus(body: UpdateMeStatusPayload, headers?: Record<string, string>): Promise<User> {
63
- return this.client.put<User, UpdateMeStatusPayload>('/me/status', body, headers)
114
+ updateStatus(
115
+ body: UpdateMeStatusPayload,
116
+ headers?: Record<string, string>,
117
+ ): Promise<MeStatusResponse> {
118
+ return this.client.put<MeStatusResponse, UpdateMeStatusPayload>('/me/status', body, headers)
64
119
  }
65
120
 
66
121
  /**
67
- * Extends the grace period for the current account when it has expired or is about to expire.
68
- * Returns the updated account.
122
+ * Extends the grace period for the current account when it has expired or is about to expire, and notifies the
123
+ * billing webhook. Returns the updated account; an account not in a grace period answers 400.
69
124
  */
70
125
  extendAccountGracePeriod(headers?: Record<string, string>): Promise<Account> {
71
126
  return this.client.post<Account>('/me/account/extend-grace-period', undefined, headers)
72
127
  }
73
128
 
74
129
  /**
75
- * Registers a OneSignal push notification token for the current user.
76
- * Used to enable push notifications on mobile/web clients.
130
+ * Registers the push notification tokens of the current user (`data` replaces the whole `data` of the user).
131
+ * Resolves with no body.
77
132
  */
78
133
  registerToken(body: RegisterTokenPayload, headers?: Record<string, string>): Promise<void> {
79
134
  return this.client.post<void, RegisterTokenPayload>(
@@ -89,8 +144,8 @@ export class MeApi extends BaseApi {
89
144
  }
90
145
 
91
146
  /**
92
- * Sets the current user's status to `online` for the given client and emits a takeover event,
93
- * which signals other connected sessions to yield control to this one.
147
+ * Sets the current user's status to `online` for the given client (`'web'` by default) and emits a takeover
148
+ * event, which signals other connected sessions to yield control to this one. Resolves with no body.
94
149
  */
95
150
  takeover(body?: TakeoverPayload, headers?: Record<string, string>): Promise<void> {
96
151
  return this.client.post<void, TakeoverPayload>('/me/takeover', body, headers)
@@ -107,9 +162,9 @@ export class MeApi extends BaseApi {
107
162
  /**
108
163
  * Activates OTP (two-factor) authentication for the current user.
109
164
  * Requires the `x-digisac-otp` header with a valid TOTP token generated from the secret key
110
- * previously set via `setOtpSecretKey`.
165
+ * previously set via `setOtpSecretKey`. Resolves the text `'OK'`.
111
166
  */
112
- activateOtpAuth(headers?: Record<string, string>): Promise<boolean> {
113
- return this.client.post<boolean>('/me/activate-otp-auth', undefined, headers)
167
+ activateOtpAuth(headers?: Record<string, string>): Promise<'OK'> {
168
+ return this.client.post<'OK'>('/me/activate-otp-auth', undefined, headers)
114
169
  }
115
170
  }
@@ -2,9 +2,68 @@ import type { AccountSettings } from '../accounts/types'
2
2
  import type { UserPreferences } from '../users/types'
3
3
  import type { UserStatus } from '../users/types'
4
4
 
5
+ /** The client of the session that sets the status: the web app or the mobile app. */
6
+ export type MeClient = 'web' | 'app'
7
+
8
+ /** Query of `GET /me`: without `include`, the user comes without relations. */
9
+ export type MeQuery = {
10
+ include?: (
11
+ | 'roles'
12
+ | 'departments'
13
+ | 'timetable'
14
+ | 'account'
15
+ | 'permissions'
16
+ | 'organizations'
17
+ | (string & {})
18
+ )[]
19
+ attributes?: string[]
20
+ }
21
+
22
+ /** Query of `GET /me/account`: `where.id` is the id of the account of the user (`accountId` of `getMe`). */
23
+ export type MeAccountQuery = {
24
+ where: { id: string }
25
+ include?: string[]
26
+ attributes?: string[]
27
+ }
28
+
29
+ export type AccountAmountsFilter = 'services.amounts' | 'users.amounts'
30
+
31
+ /** Query of `GET /me/get-account-amounts`: which amounts to answer (the credit amounts are never answered). */
32
+ export type AccountAmountsQuery = {
33
+ filter: AccountAmountsFilter[]
34
+ }
35
+
36
+ export type UsersAmounts = {
37
+ planBase: number
38
+ additional: number
39
+ totalContracted: number
40
+ planBaseConsumed: number
41
+ additionalConsumed: number
42
+ totalConsumed: number
43
+ available: number
44
+ }
45
+
46
+ export type AccountAmounts = {
47
+ accountId: string
48
+ servicesAmounts?: Record<string, unknown>
49
+ usersAmounts?: UsersAmounts
50
+ }
51
+
52
+ /** What `updateStatus` answers: the status fields of the user, not the whole user. */
53
+ export type MeStatusResponse = {
54
+ id: string
55
+ updatedAt: string
56
+ status: UserStatus
57
+ clientsStatus: Partial<Record<MeClient, UserStatus>>
58
+ language: string
59
+ account?: unknown
60
+ }
61
+
5
62
  export type UpdateMePayload = {
6
63
  name?: string
7
64
  password?: string
65
+ /** Required with `password`: the current password of the user. */
66
+ currentPassword?: string
8
67
  language?: string
9
68
  preferences?: UserPreferences
10
69
  otpAuthActive?: boolean
@@ -14,22 +73,34 @@ export type UpdateMePayload = {
14
73
 
15
74
  export type UpdateMeStatusPayload = {
16
75
  status: UserStatus
17
- client?: string
76
+ /** Required by the backend (400 without it). */
77
+ client?: MeClient
18
78
  }
19
79
 
80
+ /**
81
+ * Merged over the account. The default department is required while the tickets are enabled
82
+ * (`settings.ticketsEnabled`; 400 `DEFAULT_DEPARTMENT_REQUIRED`).
83
+ */
20
84
  export type UpdateMeAccountPayload = {
21
85
  name?: string
22
86
  settings?: Partial<AccountSettings>
23
- wizardProgress?: string
87
+ wizardProgress?: 'notStarted' | 'started' | 'finished' | (string & {})
24
88
  defaultDepartmentId?: string
89
+ defaultDepartment?: { id: string }
25
90
  }
26
91
 
27
92
  export type TakeoverPayload = {
28
- client?: string
93
+ /** Defaults to `'web'`. */
94
+ client?: MeClient
29
95
  }
30
96
 
31
97
  export type SetOtpSecretKeyPayload = {
32
98
  secretKey: string
33
99
  }
34
100
 
35
- export type RegisterTokenPayload = Record<string, unknown>
101
+ /** The push tokens of the user. `data` REPLACES the whole `data` of the user (it is not merged). */
102
+ export type RegisterTokenPayload = {
103
+ data?: { oneSignalToken?: string; expoPushToken?: string | null; fromApp?: boolean }
104
+ /** The backend ignores the call when it is `true`. */
105
+ isClientUser?: boolean
106
+ }
@@ -18,6 +18,10 @@ import type {
18
18
  import { CardsApi } from './cards/CardsApi'
19
19
  import { ContactsApi } from './contacts/ContactsApi'
20
20
  import { InteractiveMessagesApi } from './interactiveMessages/InteractiveMessagesApi'
21
+ import { MeApi } from './me/MeApi'
22
+ import type { AccountAmounts, MeStatusResponse } from './me/types'
23
+ import { TranscriptsApi } from './transcripts/TranscriptsApi'
24
+ import { WhatsappBusinessTemplatesApi } from './whatsappBusinessTemplates/WhatsappBusinessTemplatesApi'
21
25
  import type {
22
26
  ChannelRedirect,
23
27
  CtaUrlInteractiveMessageContent,
@@ -70,7 +74,7 @@ import { NotificationsApi } from './notifications/NotificationsApi'
70
74
  import { OrganizationsApi } from './organizations/OrganizationsApi'
71
75
  import { PeopleApi } from './people/PeopleApi'
72
76
  import { ServicesApi } from './services/ServicesApi'
73
- import type { UpdateUserPayload } from './users/types'
77
+ import type { UpdateUserPayload, User } from './users/types'
74
78
  import { WebhooksApi } from './webhooks/WebhooksApi'
75
79
 
76
80
  declare const client: ApiClient
@@ -210,7 +214,10 @@ expectError(
210
214
  )
211
215
 
212
216
  declare const interactiveMessage: InteractiveMessage
213
- expectType<'list' | 'button' | 'cta_url' | 'request_contact_info'>(interactiveMessage.type)
217
+ // `type` is not a field of the backend (deprecated, optional): the type is `interactive.type`.
218
+ expectType<'list' | 'button' | 'cta_url' | 'request_contact_info' | undefined>(
219
+ interactiveMessage.type,
220
+ )
214
221
  if (interactiveMessage.interactive.type === 'cta_url') {
215
222
  expectType<CtaUrlInteractiveMessageContent>(interactiveMessage.interactive)
216
223
  expectType<string>(interactiveMessage.interactive.action.parameters.url)
@@ -548,6 +555,33 @@ expectType<Promise<boolean>>(new RolesApi(client).deleteById('r-1'))
548
555
  expectType<Promise<void>>(new AcceptanceTermsApi(client).deleteById('t-1'))
549
556
  expectType<Promise<void>>(new DistributionApi(client).deleteById('d-1'))
550
557
 
558
+ // ── QARC-1009: /me, transcripts, interactive messages, templates ─────
559
+
560
+ expectType<Promise<'OK'>>(new MeApi(client).activateOtpAuth({ 'x-digisac-otp': '123456' }))
561
+ expectType<Promise<MeStatusResponse>>(
562
+ new MeApi(client).updateStatus({ status: 'offline', client: 'web' }),
563
+ )
564
+ expectType<Promise<AccountAmounts>>(
565
+ new MeApi(client).getAccountAmounts({ filter: ['users.amounts'] }),
566
+ )
567
+ expectType<Promise<User>>(new MeApi(client).getMe({ include: ['account'] }))
568
+ expectError(new MeApi(client).updateStatus({ status: 'offline', client: 'desktop' }))
569
+ expectType<Promise<void>>(new TranscriptsApi(client).transcribe('m-1'))
570
+ expectType<Promise<{ count: number }>>(new InteractiveMessagesApi(client).count())
571
+ expectType<Promise<{ count: number }>>(new WhatsappBusinessTemplatesApi(client).count())
572
+ expectType<Promise<'OK'>>(new WhatsappBusinessTemplatesApi(client).refreshTemplates())
573
+ expectError(
574
+ new WhatsappBusinessTemplatesApi(client).create({
575
+ name: 'qa',
576
+ internalName: 'QA',
577
+ components: [],
578
+ serviceId: 's-1',
579
+ messageType: 'text_only',
580
+ language: 'pt_BR',
581
+ category: 'UTILITY',
582
+ }),
583
+ )
584
+
551
585
  // ── create/updateById: what each route really answers (QARC-1008) ─────
552
586
 
553
587
  expectType<Promise<Distribution | undefined>>(
@@ -1,7 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest'
2
2
  import { TranscriptsApi } from './TranscriptsApi'
3
3
  import type { ApiClient } from '../../core/ApiClient'
4
- import type { TranscriptResponse } from './types'
5
4
 
6
5
  // ── Mock client factory ──────────────────────────────────────────────
7
6
 
@@ -28,36 +27,30 @@ describe('TranscriptsApi', () => {
28
27
  api = new TranscriptsApi(client)
29
28
  })
30
29
 
31
- // ── getById ──────────────────────────────────────────────────────
32
-
33
- describe('getById', () => {
34
- it('calls client.get with /transcripts/:id', async () => {
35
- const response: TranscriptResponse = {
36
- id: 'msg-1',
37
- isTranscribing: true,
38
- transcribeError: false,
39
- }
40
- vi.mocked(client.get).mockResolvedValue(response)
41
- const result = await api.getById('msg-1')
30
+ describe('transcribe', () => {
31
+ it('calls client.get with /transcripts/:messageId and resolves the empty answer', async () => {
32
+ vi.mocked(client.get).mockResolvedValue(undefined)
33
+ await expect(api.transcribe('msg-1')).resolves.toBeUndefined()
42
34
  expect(client.get).toHaveBeenCalledWith('/transcripts/msg-1', undefined)
43
- expect(result).toEqual(response)
44
35
  })
45
36
 
46
37
  it('forwards custom headers', async () => {
47
- const response: TranscriptResponse = {
48
- id: 'msg-2',
49
- isTranscribing: false,
50
- transcribeError: true,
51
- }
52
- vi.mocked(client.get).mockResolvedValue(response)
53
38
  const headers = { 'X-Custom': 'h' }
54
- await api.getById('msg-2', headers)
39
+ await api.transcribe('msg-2', headers)
55
40
  expect(client.get).toHaveBeenCalledWith('/transcripts/msg-2', headers)
56
41
  })
57
42
 
58
43
  it('propagates errors from client.get', async () => {
59
44
  vi.mocked(client.get).mockRejectedValue(new Error('Not found'))
60
- await expect(api.getById('missing')).rejects.toThrow('Not found')
45
+ await expect(api.transcribe('missing')).rejects.toThrow('Not found')
46
+ })
47
+ })
48
+
49
+ describe('getById (deprecated)', () => {
50
+ it('starts the transcription through the same route', async () => {
51
+ vi.mocked(client.get).mockResolvedValue(undefined)
52
+ await expect(api.getById('msg-3')).resolves.toBeUndefined()
53
+ expect(client.get).toHaveBeenCalledWith('/transcripts/msg-3', undefined)
61
54
  })
62
55
  })
63
56
  })
@@ -1,12 +1,21 @@
1
1
  import { BaseApi } from '../../core/BaseApi'
2
- import type { HasGetById } from '../../core/types'
3
- import type { TranscriptResponse } from './types'
4
2
 
5
- export class TranscriptsApi extends BaseApi implements HasGetById<TranscriptResponse> {
3
+ export class TranscriptsApi extends BaseApi {
6
4
  private readonly basePath = '/transcripts'
7
5
 
8
- /** Triggers audio transcription for a message by dispatching an async job, and marks it as transcribing. */
9
- getById(id: string, headers?: Record<string, string>): Promise<TranscriptResponse> {
10
- return this.client.get<TranscriptResponse>(`${this.basePath}/${id}`, headers)
6
+ /**
7
+ * Starts the transcription of the audio of a message: marks the message as transcribing and dispatches an async
8
+ * job, which calls the transcription service (AI) and debits the AI credits of the account. Resolves with no body;
9
+ * the result shows on the message. An unknown message answers 500.
10
+ */
11
+ transcribe(messageId: string, headers?: Record<string, string>): Promise<void> {
12
+ return this.client.get<void>(`${this.basePath}/${messageId}`, headers)
13
+ }
14
+
15
+ /**
16
+ * @deprecated Not a read: it starts the transcription of the message. Use {@link transcribe}.
17
+ */
18
+ getById(id: string, headers?: Record<string, string>): Promise<void> {
19
+ return this.transcribe(id, headers)
11
20
  }
12
21
  }
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @deprecated The route answers no body: `TranscriptsApi.transcribe` (and `getById`) resolve `undefined`. The
3
+ * transcription status shows on the message (`isTranscribing`, `transcribeError`).
4
+ */
1
5
  export type TranscriptResponse = {
2
6
  id: string
3
7
  isTranscribing: boolean
@@ -0,0 +1,86 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { WhatsappBusinessTemplatesApi } from './WhatsappBusinessTemplatesApi'
3
+ import type { ApiClient } from '../../core/ApiClient'
4
+
5
+ // ── Mock client factory ──────────────────────────────────────────────
6
+
7
+ function createMockClient(): ApiClient {
8
+ return {
9
+ setAccessToken: vi.fn().mockReturnThis(),
10
+ request: vi.fn(),
11
+ get: vi.fn(),
12
+ post: vi.fn(),
13
+ put: vi.fn(),
14
+ patch: vi.fn(),
15
+ delete: vi.fn(),
16
+ }
17
+ }
18
+
19
+ // ── Tests ────────────────────────────────────────────────────────────
20
+
21
+ describe('WhatsappBusinessTemplatesApi', () => {
22
+ let client: ReturnType<typeof createMockClient>
23
+ let api: WhatsappBusinessTemplatesApi
24
+
25
+ beforeEach(() => {
26
+ client = createMockClient()
27
+ api = new WhatsappBusinessTemplatesApi(client)
28
+ })
29
+
30
+ it('count calls client.get with /whatsapp-business-templates/count', async () => {
31
+ vi.mocked(client.get).mockResolvedValue({ count: 2 })
32
+ await expect(api.count()).resolves.toEqual({ count: 2 })
33
+ expect(client.get).toHaveBeenCalledWith('/whatsapp-business-templates/count', undefined)
34
+ })
35
+
36
+ it('getHsmLimit calls client.get with /whatsapp-business-templates/:accountId/hsmLimit', async () => {
37
+ const limit = { hsmLimit: 10, hsmUsedLimit: 3, hasLimit: 7 }
38
+ vi.mocked(client.get).mockResolvedValue(limit)
39
+ await expect(api.getHsmLimit('acc-1')).resolves.toEqual(limit)
40
+ expect(client.get).toHaveBeenCalledWith(
41
+ '/whatsapp-business-templates/acc-1/hsmLimit',
42
+ undefined,
43
+ )
44
+ })
45
+
46
+ it('sendToReview posts to /whatsapp-business-templates/:id/send-to-review', async () => {
47
+ await api.sendToReview('t-1')
48
+ expect(client.post).toHaveBeenCalledWith(
49
+ '/whatsapp-business-templates/t-1/send-to-review',
50
+ undefined,
51
+ undefined,
52
+ )
53
+ })
54
+
55
+ it('refreshTemplates posts to /whatsapp-business-templates/refresh-templates and resolves OK', async () => {
56
+ vi.mocked(client.post).mockResolvedValue('OK')
57
+ await expect(api.refreshTemplates()).resolves.toBe('OK')
58
+ expect(client.post).toHaveBeenCalledWith(
59
+ '/whatsapp-business-templates/refresh-templates',
60
+ undefined,
61
+ undefined,
62
+ )
63
+ })
64
+
65
+ it('resume calls client.get with /whatsapp-business-templates/resume', async () => {
66
+ vi.mocked(client.get).mockResolvedValue([])
67
+ await expect(api.resume()).resolves.toEqual([])
68
+ expect(client.get).toHaveBeenCalledWith('/whatsapp-business-templates/resume', undefined)
69
+ })
70
+
71
+ it('create posts the draft with status and namespace', async () => {
72
+ const body = {
73
+ name: 'qa_template',
74
+ internalName: 'QA',
75
+ status: '',
76
+ namespace: '',
77
+ components: [{ type: 'BODY' as const, text: 'Olá' }],
78
+ serviceId: 's-1',
79
+ messageType: 'text_only' as const,
80
+ language: 'pt_BR' as const,
81
+ category: 'UTILITY' as const,
82
+ }
83
+ await api.create(body)
84
+ expect(client.post).toHaveBeenCalledWith('/whatsapp-business-templates', body, undefined)
85
+ })
86
+ })