weflayr 0.22.1 → 0.23.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.
@@ -48,7 +48,7 @@ async function withClientReturning(responseBody, statusCode, run, clientOptions
48
48
 
49
49
  test('client sends the bearer key to the configured host', async () => {
50
50
  await withClientReturning({ rows: [] }, 200, async (client, seenRequests) => {
51
- const { data } = await api.getAllRevenue({ client, throwOnError: true });
51
+ const { data } = await api.getRevenue({ client, throwOnError: true });
52
52
  assert.deepEqual(data, { rows: [] });
53
53
  assert.deepEqual(seenRequests, [
54
54
  { method: 'GET', url: '/api/revenue/', authorization: 'Bearer wf-test-key', body: null },
@@ -82,7 +82,7 @@ test('client picks up apiKey from WEFLAYR_API_KEY env var', async () => {
82
82
  { rows: [] },
83
83
  200,
84
84
  async (client, seenRequests) => {
85
- await api.getAllRevenue({ client, throwOnError: true });
85
+ await api.getRevenue({ client, throwOnError: true });
86
86
  assert.equal(seenRequests[0].authorization, 'Bearer env-key');
87
87
  },
88
88
  { apiKey: undefined }
@@ -97,8 +97,8 @@ test('client picks up apiKey from WEFLAYR_API_KEY env var', async () => {
97
97
  test('setRevenue posts the rows in snake_case and parses the booked count', async () => {
98
98
  await withClientReturning({ ok: true, rows_inserted: 2 }, 200, async (client, seenRequests) => {
99
99
  const rows = [
100
- { customerName: 'c_1', amount: '1200.50', month: '2026-06' },
101
- { customerName: 'c_2', amount: '800', month: '2026-06' },
100
+ { customerName: 'c_1', amount: 1200.5, month: '2026-06' },
101
+ { customerName: 'c_2', amount: 800, month: '2026-06' },
102
102
  ];
103
103
  const { data } = await api.setRevenue({ client, throwOnError: true, body: { rows } });
104
104
  assert.deepEqual(data, { ok: true, rowsInserted: 2 });
@@ -107,70 +107,91 @@ test('setRevenue posts the rows in snake_case and parses the booked count', asyn
107
107
  assert.equal(request.url, '/api/revenue/');
108
108
  assert.deepEqual(request.body, {
109
109
  rows: [
110
- { customer_name: 'c_1', amount: '1200.50', month: '2026-06' },
111
- { customer_name: 'c_2', amount: '800', month: '2026-06' },
110
+ { customer_name: 'c_1', amount: 1200.5, month: '2026-06' },
111
+ { customer_name: 'c_2', amount: 800, month: '2026-06' },
112
112
  ],
113
113
  });
114
114
  });
115
115
  });
116
116
 
117
- test('getAllRevenue parses the returned rows into camelCase', async () => {
117
+ test('setRevenue posts the revenueId in snake_case and the upsert flag', async () => {
118
+ await withClientReturning({ ok: true, rows_inserted: 1 }, 200, async (client, seenRequests) => {
119
+ const rows = [
120
+ {
121
+ revenueId: 'b3f2b160-6e12-4f1a-9c3d-0a2e7c9b5d41',
122
+ customerName: 'c_1',
123
+ amount: 1300,
124
+ month: '2026-06',
125
+ },
126
+ ];
127
+ const { data } = await api.setRevenue({
128
+ client,
129
+ throwOnError: true,
130
+ body: { upsert: true, rows },
131
+ });
132
+ assert.deepEqual(data, { ok: true, rowsInserted: 1 });
133
+ assert.deepEqual(seenRequests[0].body, {
134
+ upsert: true,
135
+ rows: [
136
+ {
137
+ revenue_id: 'b3f2b160-6e12-4f1a-9c3d-0a2e7c9b5d41',
138
+ customer_name: 'c_1',
139
+ amount: 1300,
140
+ month: '2026-06',
141
+ },
142
+ ],
143
+ });
144
+ });
145
+ });
146
+
147
+ test('getRevenue parses the returned rows into camelCase', async () => {
118
148
  const wireRow = {
119
149
  customer_name: 'c_1',
120
150
  metric_name: 'revenue',
121
151
  month: '2026-06',
122
- amount: '1200.50000000',
152
+ amount: 1200.5,
123
153
  source: 'api',
124
154
  };
125
155
  await withClientReturning({ rows: [wireRow] }, 200, async (client) => {
126
- const { data } = await api.getAllRevenue({ client, throwOnError: true });
156
+ const { data } = await api.getRevenue({ client, throwOnError: true });
127
157
  assert.deepEqual(data.rows, [
128
158
  {
129
159
  customerName: 'c_1',
130
160
  metricName: 'revenue',
131
161
  month: '2026-06',
132
- amount: '1200.50000000',
162
+ amount: 1200.5,
133
163
  source: 'api',
134
164
  },
135
165
  ]);
136
166
  });
137
167
  });
138
168
 
139
- test('getCustomerRevenue requests the customer path', async () => {
169
+ test('getRevenue sends the customerNames filter as repeated params', async () => {
140
170
  await withClientReturning({ rows: [] }, 200, async (client, seenRequests) => {
141
- const { data } = await api.getCustomerRevenue({
171
+ const { data } = await api.getRevenue({
142
172
  client,
143
173
  throwOnError: true,
144
- path: { customerName: 'c_1' },
174
+ query: { customerNames: ['c_1', 'c_2'] },
145
175
  });
146
176
  assert.deepEqual(data, { rows: [] });
147
- assert.equal(seenRequests[0].url, '/api/revenue/c_1/');
177
+ const query = new URL(seenRequests[0].url, 'http://x').searchParams;
178
+ assert.deepEqual(query.getAll('customer_names'), ['c_1', 'c_2']);
148
179
  });
149
180
  });
150
181
 
151
- test('getCustomerRevenue surfaces a 404 and its error', async () => {
152
- const rejection = { error: 'No revenue for this customer.' };
153
- await withClientReturning(rejection, 404, async (client) => {
154
- const { error, response } = await api.getCustomerRevenue({
155
- client,
156
- path: { customerName: 'nobody' },
157
- });
158
- assert.equal(response.status, 404);
159
- assert.deepEqual(error, rejection);
160
- });
161
- });
162
-
163
- test('deleteCustomerRevenue parses the deleted count', async () => {
164
- await withClientReturning({ ok: true, rows_deleted: 1 }, 200, async (client, seenRequests) => {
165
- const { data } = await api.deleteCustomerRevenue({
182
+ test('deleteRevenue sends the customerNames filter and parses the deleted count', async () => {
183
+ await withClientReturning({ ok: true, rows_deleted: 2 }, 200, async (client, seenRequests) => {
184
+ const { data } = await api.deleteRevenue({
166
185
  client,
167
186
  throwOnError: true,
168
- path: { customerName: 'c_1' },
187
+ query: { customerNames: ['c_1', 'c_2'] },
169
188
  });
170
- assert.deepEqual(data, { ok: true, rowsDeleted: 1 });
189
+ assert.deepEqual(data, { ok: true, rowsDeleted: 2 });
171
190
  const request = seenRequests[0];
172
191
  assert.equal(request.method, 'DELETE');
173
- assert.equal(request.url, '/api/revenue/c_1/');
192
+ const url = new URL(request.url, 'http://x');
193
+ assert.equal(url.pathname, '/api/revenue/');
194
+ assert.deepEqual(url.searchParams.getAll('customer_names'), ['c_1', 'c_2']);
174
195
  });
175
196
  });
176
197
 
@@ -178,51 +199,50 @@ test('deleteCustomerRevenue parses the deleted count', async () => {
178
199
 
179
200
  test('setKeyMetrics posts the rows in snake_case and parses the booked count', async () => {
180
201
  await withClientReturning({ ok: true, rows_inserted: 1 }, 200, async (client, seenRequests) => {
181
- const rows = [
182
- { userName: 'u_1', metricName: 'Meetings booked', amount: '3', month: '2026-06' },
183
- ];
202
+ const rows = [{ userName: 'u_1', metricName: 'Meetings booked', amount: 3, month: '2026-06' }];
184
203
  const { data } = await api.setKeyMetrics({ client, throwOnError: true, body: { rows } });
185
204
  assert.deepEqual(data, { ok: true, rowsInserted: 1 });
186
205
  const request = seenRequests[0];
187
206
  assert.equal(request.method, 'POST');
188
207
  assert.equal(request.url, '/api/key-metrics/');
189
208
  assert.deepEqual(request.body, {
190
- rows: [{ user_name: 'u_1', metric_name: 'Meetings booked', amount: '3', month: '2026-06' }],
209
+ rows: [{ user_name: 'u_1', metric_name: 'Meetings booked', amount: 3, month: '2026-06' }],
191
210
  });
192
211
  });
193
212
  });
194
213
 
195
- test('getAllKeyMetrics parses the returned rows into camelCase', async () => {
214
+ test('getKeyMetrics parses the returned rows into camelCase', async () => {
196
215
  const wireRow = {
197
216
  user_name: 'u_1',
198
217
  metric_name: 'Meetings booked',
199
218
  month: '2026-06',
200
- amount: '3.00000000',
219
+ amount: 3.0,
201
220
  source: 'api',
202
221
  };
203
222
  await withClientReturning({ rows: [wireRow] }, 200, async (client) => {
204
- const { data } = await api.getAllKeyMetrics({ client, throwOnError: true });
223
+ const { data } = await api.getKeyMetrics({ client, throwOnError: true });
205
224
  assert.deepEqual(data.rows, [
206
225
  {
207
226
  userName: 'u_1',
208
227
  metricName: 'Meetings booked',
209
228
  month: '2026-06',
210
- amount: '3.00000000',
229
+ amount: 3.0,
211
230
  source: 'api',
212
231
  },
213
232
  ]);
214
233
  });
215
234
  });
216
235
 
217
- test('getUserKeyMetrics requests the user path', async () => {
236
+ test('getKeyMetrics sends the userNames filter as repeated params', async () => {
218
237
  await withClientReturning({ rows: [] }, 200, async (client, seenRequests) => {
219
- const { data } = await api.getUserKeyMetrics({
238
+ const { data } = await api.getKeyMetrics({
220
239
  client,
221
240
  throwOnError: true,
222
- path: { userName: 'u_1' },
241
+ query: { userNames: ['u_1', 'u_2'] },
223
242
  });
224
243
  assert.deepEqual(data, { rows: [] });
225
- assert.equal(seenRequests[0].url, '/api/key-metrics/u_1/');
244
+ const query = new URL(seenRequests[0].url, 'http://x').searchParams;
245
+ assert.deepEqual(query.getAll('user_names'), ['u_1', 'u_2']);
226
246
  });
227
247
  });
228
248
 
@@ -238,17 +258,95 @@ test('setKeyMetrics surfaces a 422 and its details', async () => {
238
258
  });
239
259
  });
240
260
 
241
- test('deleteUserKeyMetrics parses the deleted count', async () => {
242
- await withClientReturning({ ok: true, rows_deleted: 1 }, 200, async (client, seenRequests) => {
243
- const { data } = await api.deleteUserKeyMetrics({
261
+ test('deleteKeyMetrics sends the userNames filter and parses the deleted count', async () => {
262
+ await withClientReturning({ ok: true, rows_deleted: 2 }, 200, async (client, seenRequests) => {
263
+ const { data } = await api.deleteKeyMetrics({
244
264
  client,
245
265
  throwOnError: true,
246
- path: { userName: 'u_1' },
266
+ query: { userNames: ['u_1', 'u_2'] },
247
267
  });
248
- assert.deepEqual(data, { ok: true, rowsDeleted: 1 });
268
+ assert.deepEqual(data, { ok: true, rowsDeleted: 2 });
249
269
  const request = seenRequests[0];
250
270
  assert.equal(request.method, 'DELETE');
251
- assert.equal(request.url, '/api/key-metrics/u_1/');
271
+ const url = new URL(request.url, 'http://x');
272
+ assert.equal(url.pathname, '/api/key-metrics/');
273
+ assert.deepEqual(url.searchParams.getAll('user_names'), ['u_1', 'u_2']);
274
+ });
275
+ });
276
+
277
+ // ── Margin ────────────────────────────────────────────────────────────────────
278
+
279
+ test('getMargin parses the returned rows into camelCase', async () => {
280
+ const wireRow = {
281
+ customer_name: 'c_1',
282
+ revenue: 1000.0,
283
+ cost: 200.0,
284
+ margin: 800.0,
285
+ margin_pct: 80.0,
286
+ profitability_status: 'healthy',
287
+ };
288
+ await withClientReturning(
289
+ { rows: [wireRow], next_cursor: null, has_more: false },
290
+ 200,
291
+ async (client) => {
292
+ const { data } = await api.getMargin({ client, throwOnError: true });
293
+ assert.deepEqual(data.rows, [
294
+ {
295
+ customerName: 'c_1',
296
+ revenue: 1000.0,
297
+ cost: 200.0,
298
+ margin: 800.0,
299
+ marginPct: 80.0,
300
+ profitabilityStatus: 'healthy',
301
+ },
302
+ ]);
303
+ }
304
+ );
305
+ });
306
+
307
+ test('getMargin sends the cursor and limit query params', async () => {
308
+ await withClientReturning(
309
+ { rows: [], next_cursor: 'Y19h', has_more: true },
310
+ 200,
311
+ async (client, seenRequests) => {
312
+ const { data } = await api.getMargin({
313
+ client,
314
+ throwOnError: true,
315
+ query: { cursor: 'abc123', limit: 2 },
316
+ });
317
+ assert.equal(data.nextCursor, 'Y19h');
318
+ assert.equal(data.hasMore, true);
319
+ const query = new URL(seenRequests[0].url, 'http://x').searchParams;
320
+ assert.equal(query.get('cursor'), 'abc123');
321
+ assert.equal(query.get('limit'), '2');
322
+ }
323
+ );
324
+ });
325
+
326
+ test('getMargin sends the customerNames filter as repeated params', async () => {
327
+ await withClientReturning({ rows: [] }, 200, async (client, seenRequests) => {
328
+ const { data } = await api.getMargin({
329
+ client,
330
+ throwOnError: true,
331
+ query: { customerNames: ['c_1', 'c_2'] },
332
+ });
333
+ assert.deepEqual(data, { rows: [] });
334
+ const query = new URL(seenRequests[0].url, 'http://x').searchParams;
335
+ assert.deepEqual(query.getAll('customer_names'), ['c_1', 'c_2']);
336
+ });
337
+ });
338
+
339
+ test('getMargin sends the dateFrom/dateTo/applyCredits filters in snake_case', async () => {
340
+ await withClientReturning({ rows: [] }, 200, async (client, seenRequests) => {
341
+ await api.getMargin({
342
+ client,
343
+ throwOnError: true,
344
+ query: { dateFrom: '2026-06-01', dateTo: '2026-06-30', applyCredits: true },
345
+ });
346
+ const query = new URL(seenRequests[0].url, 'http://x').searchParams;
347
+ assert.equal(query.get('date_from'), '2026-06-01');
348
+ assert.equal(query.get('date_to'), '2026-06-30');
349
+ assert.equal(query.get('apply_credits'), 'true');
252
350
  });
253
351
  });
254
352
 
@@ -290,41 +388,50 @@ test('setCustomersTags leaves tag keys untouched, even when they contain undersc
290
388
  );
291
389
  });
292
390
 
293
- test('getAllCustomersTags parses the returned customers into camelCase', async () => {
391
+ test('getCustomersTags parses the returned customers into camelCase', async () => {
294
392
  const wireCustomer = { customer_name: 'c_1', tags: { plan: 'pro' } };
295
393
  await withClientReturning(
296
394
  { customers: [wireCustomer], next_cursor: null, has_more: false },
297
395
  200,
298
396
  async (client) => {
299
- const { data } = await api.getAllCustomersTags({ client, throwOnError: true });
397
+ const { data } = await api.getCustomersTags({ client, throwOnError: true });
300
398
  assert.deepEqual(data.customers, [{ customerName: 'c_1', tags: { plan: 'pro' } }]);
301
399
  }
302
400
  );
303
401
  });
304
402
 
305
- test('getCustomerTags parses the customer tags, leaving tag keys untouched', async () => {
306
- const wireTags = { customer_name: 'c_1', tags: { plan: 'pro', utm_source: 'ads' } };
307
- await withClientReturning(wireTags, 200, async (client, seenRequests) => {
308
- const { data } = await api.getCustomerTags({
309
- client,
310
- throwOnError: true,
311
- path: { customerName: 'c_1' },
312
- });
313
- assert.deepEqual(data, { customerName: 'c_1', tags: { plan: 'pro', utm_source: 'ads' } });
314
- assert.equal(seenRequests[0].url, '/api/customer-tags/c_1/');
315
- });
403
+ test('getCustomersTags sends the customerNames filter, leaving tag keys untouched', async () => {
404
+ const wireCustomer = { customer_name: 'c_1', tags: { plan: 'pro', utm_source: 'ads' } };
405
+ await withClientReturning(
406
+ { customers: [wireCustomer], next_cursor: null, has_more: false },
407
+ 200,
408
+ async (client, seenRequests) => {
409
+ const { data } = await api.getCustomersTags({
410
+ client,
411
+ throwOnError: true,
412
+ query: { customerNames: ['c_1', 'c_2'] },
413
+ });
414
+ assert.deepEqual(data.customers, [
415
+ { customerName: 'c_1', tags: { plan: 'pro', utm_source: 'ads' } },
416
+ ]);
417
+ const query = new URL(seenRequests[0].url, 'http://x').searchParams;
418
+ assert.deepEqual(query.getAll('customer_names'), ['c_1', 'c_2']);
419
+ }
420
+ );
316
421
  });
317
422
 
318
- test('deleteCustomerTags parses the deleted count', async () => {
319
- await withClientReturning({ ok: true, tags_deleted: 1 }, 200, async (client, seenRequests) => {
320
- const { data } = await api.deleteCustomerTags({
423
+ test('deleteCustomersTags sends the customerNames filter and parses the deleted count', async () => {
424
+ await withClientReturning({ ok: true, tags_deleted: 3 }, 200, async (client, seenRequests) => {
425
+ const { data } = await api.deleteCustomersTags({
321
426
  client,
322
427
  throwOnError: true,
323
- path: { customerName: 'c_1' },
428
+ query: { customerNames: ['c_1', 'c_2'] },
324
429
  });
325
- assert.deepEqual(data, { ok: true, tagsDeleted: 1 });
430
+ assert.deepEqual(data, { ok: true, tagsDeleted: 3 });
326
431
  const request = seenRequests[0];
327
432
  assert.equal(request.method, 'DELETE');
328
- assert.equal(request.url, '/api/customer-tags/c_1/');
433
+ const url = new URL(request.url, 'http://x');
434
+ assert.equal(url.pathname, '/api/customer-tags/');
435
+ assert.deepEqual(url.searchParams.getAll('customer_names'), ['c_1', 'c_2']);
329
436
  });
330
437
  });