@hed-hog/billing 0.0.2 → 0.0.286

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 (51) hide show
  1. package/README.md +420 -0
  2. package/dist/billing-contracts.controller.d.ts +85 -4
  3. package/dist/billing-contracts.controller.d.ts.map +1 -1
  4. package/dist/billing-coupons.controller.d.ts +60 -4
  5. package/dist/billing-coupons.controller.d.ts.map +1 -1
  6. package/dist/billing-dashboard.controller.d.ts +20 -5
  7. package/dist/billing-dashboard.controller.d.ts.map +1 -1
  8. package/dist/billing-entitlements.controller.d.ts +28 -2
  9. package/dist/billing-entitlements.controller.d.ts.map +1 -1
  10. package/dist/billing-gateways.controller.d.ts +18 -2
  11. package/dist/billing-gateways.controller.d.ts.map +1 -1
  12. package/dist/billing-invoices.controller.d.ts +56 -2
  13. package/dist/billing-invoices.controller.d.ts.map +1 -1
  14. package/dist/billing-offers.controller.d.ts +61 -4
  15. package/dist/billing-offers.controller.d.ts.map +1 -1
  16. package/dist/billing-orders.controller.d.ts +39 -2
  17. package/dist/billing-orders.controller.d.ts.map +1 -1
  18. package/dist/billing-payments.controller.d.ts +16 -1
  19. package/dist/billing-payments.controller.d.ts.map +1 -1
  20. package/dist/billing-prices.controller.d.ts +80 -4
  21. package/dist/billing-prices.controller.d.ts.map +1 -1
  22. package/dist/billing-products.controller.d.ts +62 -4
  23. package/dist/billing-products.controller.d.ts.map +1 -1
  24. package/dist/billing-subscriptions.controller.d.ts +138 -5
  25. package/dist/billing-subscriptions.controller.d.ts.map +1 -1
  26. package/dist/billing.service.d.ts +663 -39
  27. package/dist/billing.service.d.ts.map +1 -1
  28. package/dist/billing.service.js +135 -16
  29. package/dist/billing.service.js.map +1 -1
  30. package/hedhog/data/menu.yaml +1 -1
  31. package/hedhog/frontend/app/contracts/page.tsx.ejs +68 -64
  32. package/hedhog/frontend/app/coupons/page.tsx.ejs +81 -77
  33. package/hedhog/frontend/app/entitlements/page.tsx.ejs +59 -58
  34. package/hedhog/frontend/app/gateways/page.tsx.ejs +125 -57
  35. package/hedhog/frontend/app/invoices/page.tsx.ejs +49 -43
  36. package/hedhog/frontend/app/offers/page.tsx.ejs +68 -64
  37. package/hedhog/frontend/app/orders/page.tsx.ejs +47 -46
  38. package/hedhog/frontend/app/page.tsx.ejs +186 -186
  39. package/hedhog/frontend/app/payments/page.tsx.ejs +51 -45
  40. package/hedhog/frontend/app/prices/page.tsx.ejs +81 -75
  41. package/hedhog/frontend/app/products/page.tsx.ejs +79 -73
  42. package/hedhog/frontend/app/refunds/page.tsx.ejs +50 -44
  43. package/hedhog/frontend/app/reports/page.tsx.ejs +1 -1
  44. package/hedhog/frontend/app/seats/page.tsx.ejs +826 -0
  45. package/hedhog/frontend/app/subscriptions/page.tsx.ejs +95 -90
  46. package/hedhog/frontend/app/webhooks/page.tsx.ejs +47 -39
  47. package/hedhog/frontend/messages/en.json +640 -551
  48. package/hedhog/frontend/messages/pt.json +652 -563
  49. package/hedhog/table/billing_payment_method.yaml +1 -1
  50. package/package.json +4 -3
  51. package/src/billing.service.ts +299 -17
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
+ EmptyState,
4
5
  Page,
5
6
  PageHeader,
6
7
  PaginationFooter,
@@ -52,7 +53,7 @@ import {
52
53
  } from '@/components/ui/table';
53
54
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
54
55
  import { zodResolver } from '@hookform/resolvers/zod';
55
- import { Pencil, Plus, Trash2 } from 'lucide-react';
56
+ import { FileText, Pencil, Plus, Trash2 } from 'lucide-react';
56
57
  import { useTranslations } from 'next-intl';
57
58
  import { useEffect, useState } from 'react';
58
59
  import { useForm } from 'react-hook-form';
@@ -131,7 +132,7 @@ const mapDiscountTypeFromApi = (
131
132
  };
132
133
 
133
134
  export default function BillingCouponsPage() {
134
- const t = useTranslations('BillingCouponsPage');
135
+ const t = useTranslations('billing.BillingCouponsPage');
135
136
  const { request, currentLocaleCode, showToastHandler } = useApp();
136
137
  const [search, setSearch] = useState('');
137
138
  const [statusFilter, setStatusFilter] = useState('all');
@@ -332,83 +333,86 @@ export default function BillingCouponsPage() {
332
333
  }}
333
334
  />
334
335
 
335
- <div className="overflow-x-auto rounded-md border">
336
- <Table>
337
- <TableHeader>
338
- <TableRow>
339
- <TableHead>{t('table.columns.code')}</TableHead>
340
- <TableHead>{t('table.columns.name')}</TableHead>
341
- <TableHead>{t('table.columns.discountType')}</TableHead>
342
- <TableHead>{t('table.columns.discountValue')}</TableHead>
343
- <TableHead>{t('table.columns.uses')}</TableHead>
344
- <TableHead>{t('table.columns.status')}</TableHead>
345
- <TableHead className="w-[120px] text-right">
346
- {t('table.columns.actions')}
347
- </TableHead>
348
- </TableRow>
349
- </TableHeader>
350
- <TableBody>
351
- {items.length === 0 && (
336
+ {items.length > 0 ? (
337
+ <div className="overflow-x-auto rounded-md border">
338
+ <Table>
339
+ <TableHeader>
352
340
  <TableRow>
353
- <TableCell
354
- colSpan={7}
355
- className="text-center text-muted-foreground"
356
- >
357
- {t('table.empty')}
358
- </TableCell>
341
+ <TableHead>{t('table.columns.code')}</TableHead>
342
+ <TableHead>{t('table.columns.name')}</TableHead>
343
+ <TableHead>{t('table.columns.discountType')}</TableHead>
344
+ <TableHead>{t('table.columns.discountValue')}</TableHead>
345
+ <TableHead>{t('table.columns.uses')}</TableHead>
346
+ <TableHead>{t('table.columns.status')}</TableHead>
347
+ <TableHead className="w-[120px] text-right">
348
+ {t('table.columns.actions')}
349
+ </TableHead>
359
350
  </TableRow>
360
- )}
361
- {items.map((item) => (
362
- <TableRow key={item.id}>
363
- <TableCell className="font-medium">{item.code}</TableCell>
364
- <TableCell>{item.name}</TableCell>
365
- <TableCell>
366
- <Badge className={badgeClass(item.discount_type)}>
367
- {item.discount_type}
368
- </Badge>
369
- </TableCell>
370
- <TableCell>
371
- {item.discount_type === 'fixed'
372
- ? formatCurrency(
373
- Math.round(item.discount_value),
374
- item.currency
375
- )
376
- : `${item.discount_value}%`}
377
- </TableCell>
378
- <TableCell>
379
- {item.uses_count}/{item.max_uses ?? '-'}
380
- </TableCell>
381
- <TableCell>
382
- <Badge className={badgeClass(item.status)}>
383
- {item.status}
384
- </Badge>
385
- </TableCell>
386
- <TableCell>
387
- <div className="flex justify-end gap-2">
388
- <Button
389
- variant="outline"
390
- size="icon"
391
- onClick={() => {
392
- setEditingCoupon(item);
393
- setSheetOpen(true);
394
- }}
395
- >
396
- <Pencil className="size-4" />
397
- </Button>
398
- <Button
399
- variant="destructive"
400
- size="icon"
401
- onClick={() => setDeleteId(item.id)}
402
- >
403
- <Trash2 className="size-4" />
404
- </Button>
405
- </div>
406
- </TableCell>
407
- </TableRow>
408
- ))}
409
- </TableBody>
410
- </Table>
411
- </div>
351
+ </TableHeader>
352
+ <TableBody>
353
+ {items.map((item) => (
354
+ <TableRow key={item.id}>
355
+ <TableCell className="font-medium">{item.code}</TableCell>
356
+ <TableCell>{item.name}</TableCell>
357
+ <TableCell>
358
+ <Badge className={badgeClass(item.discount_type)}>
359
+ {item.discount_type}
360
+ </Badge>
361
+ </TableCell>
362
+ <TableCell>
363
+ {item.discount_type === 'fixed'
364
+ ? formatCurrency(
365
+ Math.round(item.discount_value),
366
+ item.currency
367
+ )
368
+ : `${item.discount_value}%`}
369
+ </TableCell>
370
+ <TableCell>
371
+ {item.uses_count}/{item.max_uses ?? '-'}
372
+ </TableCell>
373
+ <TableCell>
374
+ <Badge className={badgeClass(item.status)}>
375
+ {item.status}
376
+ </Badge>
377
+ </TableCell>
378
+ <TableCell>
379
+ <div className="flex justify-end gap-2">
380
+ <Button
381
+ variant="outline"
382
+ size="icon"
383
+ onClick={() => {
384
+ setEditingCoupon(item);
385
+ setSheetOpen(true);
386
+ }}
387
+ >
388
+ <Pencil className="size-4" />
389
+ </Button>
390
+ <Button
391
+ variant="destructive"
392
+ size="icon"
393
+ onClick={() => setDeleteId(item.id)}
394
+ >
395
+ <Trash2 className="size-4" />
396
+ </Button>
397
+ </div>
398
+ </TableCell>
399
+ </TableRow>
400
+ ))}
401
+ </TableBody>
402
+ </Table>
403
+ </div>
404
+ ) : (
405
+ <EmptyState
406
+ icon={<FileText className="h-12 w-12" />}
407
+ title={t('table.empty')}
408
+ description={t('description')}
409
+ actionLabel={t('actions.create')}
410
+ onAction={() => {
411
+ setEditingCoupon(null);
412
+ setSheetOpen(true);
413
+ }}
414
+ />
415
+ )}
412
416
 
413
417
  <PaginationFooter
414
418
  currentPage={page}
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
+ EmptyState,
4
5
  Page,
5
6
  PageHeader,
6
7
  PaginationFooter,
@@ -51,7 +52,7 @@ import {
51
52
  } from '@/components/ui/table';
52
53
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
53
54
  import { zodResolver } from '@hookform/resolvers/zod';
54
- import { Plus, Trash2 } from 'lucide-react';
55
+ import { FileText, Plus, Trash2 } from 'lucide-react';
55
56
  import { useTranslations } from 'next-intl';
56
57
  import { useState } from 'react';
57
58
  import { useForm } from 'react-hook-form';
@@ -104,7 +105,7 @@ const badgeClass = (value: string) => {
104
105
  };
105
106
 
106
107
  export default function BillingEntitlementsPage() {
107
- const t = useTranslations('BillingEntitlementsPage');
108
+ const t = useTranslations('billing.BillingEntitlementsPage');
108
109
  const { request, currentLocaleCode, showToastHandler } = useApp();
109
110
  const [search, setSearch] = useState('');
110
111
  const [page, setPage] = useState(1);
@@ -237,64 +238,64 @@ export default function BillingEntitlementsPage() {
237
238
  placeholder={t('filters.searchPlaceholder')}
238
239
  />
239
240
 
240
- <div className="overflow-x-auto rounded-md border">
241
- <Table>
242
- <TableHeader>
243
- <TableRow>
244
- <TableHead>{t('table.columns.contactId')}</TableHead>
245
- <TableHead>{t('table.columns.sourceType')}</TableHead>
246
- <TableHead>{t('table.columns.targetType')}</TableHead>
247
- <TableHead>{t('table.columns.accessType')}</TableHead>
248
- <TableHead>{t('table.columns.status')}</TableHead>
249
- <TableHead>{t('table.columns.startsAt')}</TableHead>
250
- <TableHead>{t('table.columns.endsAt')}</TableHead>
251
- <TableHead className="w-[100px] text-right">
252
- {t('table.columns.actions')}
253
- </TableHead>
254
- </TableRow>
255
- </TableHeader>
256
- <TableBody>
257
- {items.length === 0 && (
241
+ {items.length > 0 ? (
242
+ <div className="overflow-x-auto rounded-md border">
243
+ <Table>
244
+ <TableHeader>
258
245
  <TableRow>
259
- <TableCell
260
- colSpan={8}
261
- className="text-center text-muted-foreground"
262
- >
263
- {t('table.empty')}
264
- </TableCell>
246
+ <TableHead>{t('table.columns.contactId')}</TableHead>
247
+ <TableHead>{t('table.columns.sourceType')}</TableHead>
248
+ <TableHead>{t('table.columns.targetType')}</TableHead>
249
+ <TableHead>{t('table.columns.accessType')}</TableHead>
250
+ <TableHead>{t('table.columns.status')}</TableHead>
251
+ <TableHead>{t('table.columns.startsAt')}</TableHead>
252
+ <TableHead>{t('table.columns.endsAt')}</TableHead>
253
+ <TableHead className="w-[100px] text-right">
254
+ {t('table.columns.actions')}
255
+ </TableHead>
265
256
  </TableRow>
266
- )}
267
- {items.map((item) => (
268
- <TableRow key={item.id}>
269
- <TableCell>{item.contact_id}</TableCell>
270
- <TableCell>{item.source_type}</TableCell>
271
- <TableCell>{item.target_type}</TableCell>
272
- <TableCell>
273
- <Badge className={badgeClass(item.access_type)}>
274
- {item.access_type}
275
- </Badge>
276
- </TableCell>
277
- <TableCell>
278
- <Badge className={badgeClass(item.status)}>
279
- {item.status}
280
- </Badge>
281
- </TableCell>
282
- <TableCell>{formatDate(item.starts_at)}</TableCell>
283
- <TableCell>{formatDate(item.ends_at)}</TableCell>
284
- <TableCell className="text-right">
285
- <Button
286
- variant="destructive"
287
- size="icon"
288
- onClick={() => setDeleteId(item.id)}
289
- >
290
- <Trash2 className="size-4" />
291
- </Button>
292
- </TableCell>
293
- </TableRow>
294
- ))}
295
- </TableBody>
296
- </Table>
297
- </div>
257
+ </TableHeader>
258
+ <TableBody>
259
+ {items.map((item) => (
260
+ <TableRow key={item.id}>
261
+ <TableCell>{item.contact_id}</TableCell>
262
+ <TableCell>{item.source_type}</TableCell>
263
+ <TableCell>{item.target_type}</TableCell>
264
+ <TableCell>
265
+ <Badge className={badgeClass(item.access_type)}>
266
+ {item.access_type}
267
+ </Badge>
268
+ </TableCell>
269
+ <TableCell>
270
+ <Badge className={badgeClass(item.status)}>
271
+ {item.status}
272
+ </Badge>
273
+ </TableCell>
274
+ <TableCell>{formatDate(item.starts_at)}</TableCell>
275
+ <TableCell>{formatDate(item.ends_at)}</TableCell>
276
+ <TableCell className="text-right">
277
+ <Button
278
+ variant="destructive"
279
+ size="icon"
280
+ onClick={() => setDeleteId(item.id)}
281
+ >
282
+ <Trash2 className="size-4" />
283
+ </Button>
284
+ </TableCell>
285
+ </TableRow>
286
+ ))}
287
+ </TableBody>
288
+ </Table>
289
+ </div>
290
+ ) : (
291
+ <EmptyState
292
+ icon={<FileText className="h-12 w-12" />}
293
+ title={t('table.empty')}
294
+ description={t('description')}
295
+ actionLabel={t('actions.create')}
296
+ onAction={() => setSheetOpen(true)}
297
+ />
298
+ )}
298
299
 
299
300
  <PaginationFooter
300
301
  currentPage={page}
@@ -1,15 +1,14 @@
1
1
  'use client';
2
2
 
3
- import { Page, PageHeader } from '@/components/entity-list';
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PageHeader,
7
+ PaginationFooter,
8
+ SearchBar,
9
+ } from '@/components/entity-list';
4
10
  import { Badge } from '@/components/ui/badge';
5
11
  import { Button } from '@/components/ui/button';
6
- import {
7
- Card,
8
- CardContent,
9
- CardDescription,
10
- CardHeader,
11
- CardTitle,
12
- } from '@/components/ui/card';
13
12
  import {
14
13
  Form,
15
14
  FormControl,
@@ -26,11 +25,19 @@ import {
26
25
  SheetHeader,
27
26
  SheetTitle,
28
27
  } from '@/components/ui/sheet';
28
+ import {
29
+ Table,
30
+ TableBody,
31
+ TableCell,
32
+ TableHead,
33
+ TableHeader,
34
+ TableRow,
35
+ } from '@/components/ui/table';
29
36
  import { Switch } from '@/components/ui/switch';
30
37
  import { Textarea } from '@/components/ui/textarea';
31
38
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
32
39
  import { zodResolver } from '@hookform/resolvers/zod';
33
- import { Settings } from 'lucide-react';
40
+ import { FileText, Settings } from 'lucide-react';
34
41
  import { useTranslations } from 'next-intl';
35
42
  import { useEffect, useState } from 'react';
36
43
  import { useForm } from 'react-hook-form';
@@ -71,8 +78,11 @@ const fallbackGateways: Gateway[] = [
71
78
  ];
72
79
 
73
80
  export default function BillingGatewaysPage() {
74
- const t = useTranslations('BillingGatewaysPage');
81
+ const t = useTranslations('billing.BillingGatewaysPage');
75
82
  const { request, currentLocaleCode, showToastHandler } = useApp();
83
+ const [search, setSearch] = useState('');
84
+ const [page, setPage] = useState(1);
85
+ const [pageSize, setPageSize] = useState(12);
76
86
  const [sheetOpen, setSheetOpen] = useState(false);
77
87
  const [editingGateway, setEditingGateway] = useState<Gateway | null>(null);
78
88
 
@@ -104,6 +114,19 @@ export default function BillingGatewaysPage() {
104
114
  });
105
115
 
106
116
  const items = data ?? fallbackGateways;
117
+ const query = search.trim().toLowerCase();
118
+ const filteredItems = items.filter((gateway) => {
119
+ if (!query) {
120
+ return true;
121
+ }
122
+
123
+ return [gateway.name, gateway.slug].some((value) =>
124
+ value.toLowerCase().includes(query)
125
+ );
126
+ });
127
+ const totalItems = filteredItems.length;
128
+ const start = (page - 1) * pageSize;
129
+ const pagedItems = filteredItems.slice(start, start + pageSize);
107
130
 
108
131
  useEffect(() => {
109
132
  if (!sheetOpen || !editingGateway) {
@@ -174,54 +197,99 @@ export default function BillingGatewaysPage() {
174
197
  ]}
175
198
  />
176
199
 
177
- <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
178
- {items.map((gateway) => (
179
- <Card key={gateway.slug} className="shadow-none">
180
- <CardHeader>
181
- <CardTitle className="flex items-center justify-between text-lg">
182
- {gateway.name}
183
- <Badge
184
- className={
185
- gateway.is_active
186
- ? 'bg-green-100 text-green-800'
187
- : 'bg-gray-100 text-gray-800'
188
- }
189
- >
190
- {gateway.is_active
191
- ? t('status.active')
192
- : t('status.inactive')}
193
- </Badge>
194
- </CardTitle>
195
- <CardDescription>{gateway.slug}</CardDescription>
196
- </CardHeader>
197
- <CardContent className="space-y-4">
198
- <div className="flex items-center justify-between">
199
- <span className="text-sm text-muted-foreground">
200
- {t('fields.active')}
201
- </span>
202
- <Switch
203
- checked={gateway.is_active}
204
- onCheckedChange={(checked) =>
205
- void quickToggle(gateway, checked)
206
- }
207
- />
208
- </div>
200
+ <SearchBar
201
+ searchQuery={search}
202
+ onSearchChange={(value) => {
203
+ setSearch(value);
204
+ setPage(1);
205
+ }}
206
+ onSearch={() => setPage(1)}
207
+ placeholder={t('filters.searchPlaceholder')}
208
+ />
209
209
 
210
- <Button
211
- className="w-full"
212
- variant="outline"
213
- onClick={() => {
214
- setEditingGateway(gateway);
215
- setSheetOpen(true);
216
- }}
217
- >
218
- <Settings className="mr-2 size-4" />
219
- {t('actions.configure')}
220
- </Button>
221
- </CardContent>
222
- </Card>
223
- ))}
224
- </div>
210
+ {pagedItems.length > 0 ? (
211
+ <div className="overflow-x-auto rounded-md border">
212
+ <Table>
213
+ <TableHeader>
214
+ <TableRow>
215
+ <TableHead>{t('table.columns.name')}</TableHead>
216
+ <TableHead>{t('table.columns.slug')}</TableHead>
217
+ <TableHead>{t('table.columns.status')}</TableHead>
218
+ <TableHead>{t('table.columns.active')}</TableHead>
219
+ <TableHead className="text-right">
220
+ {t('table.columns.actions')}
221
+ </TableHead>
222
+ </TableRow>
223
+ </TableHeader>
224
+ <TableBody>
225
+ {pagedItems.map((gateway) => (
226
+ <TableRow key={gateway.slug}>
227
+ <TableCell className="font-medium">{gateway.name}</TableCell>
228
+ <TableCell>{gateway.slug}</TableCell>
229
+ <TableCell>
230
+ <Badge
231
+ className={
232
+ gateway.is_active
233
+ ? 'bg-green-100 text-green-800'
234
+ : 'bg-gray-100 text-gray-800'
235
+ }
236
+ >
237
+ {gateway.is_active
238
+ ? t('status.active')
239
+ : t('status.inactive')}
240
+ </Badge>
241
+ </TableCell>
242
+ <TableCell>
243
+ <Switch
244
+ checked={gateway.is_active}
245
+ onCheckedChange={(checked) =>
246
+ void quickToggle(gateway, checked)
247
+ }
248
+ />
249
+ </TableCell>
250
+ <TableCell>
251
+ <div className="flex justify-end">
252
+ <Button
253
+ variant="outline"
254
+ size="sm"
255
+ onClick={() => {
256
+ setEditingGateway(gateway);
257
+ setSheetOpen(true);
258
+ }}
259
+ >
260
+ <Settings className="mr-2 size-4" />
261
+ {t('actions.configure')}
262
+ </Button>
263
+ </div>
264
+ </TableCell>
265
+ </TableRow>
266
+ ))}
267
+ </TableBody>
268
+ </Table>
269
+ </div>
270
+ ) : (
271
+ <EmptyState
272
+ icon={<FileText className="h-12 w-12" />}
273
+ title={t('table.empty')}
274
+ description={t('description')}
275
+ actionLabel={t('emptyAction')}
276
+ onAction={() => {
277
+ setSearch('');
278
+ setPage(1);
279
+ }}
280
+ />
281
+ )}
282
+
283
+ <PaginationFooter
284
+ currentPage={page}
285
+ pageSize={pageSize}
286
+ totalItems={totalItems}
287
+ onPageChange={setPage}
288
+ onPageSizeChange={(nextSize) => {
289
+ setPageSize(nextSize);
290
+ setPage(1);
291
+ }}
292
+ />
225
293
 
226
294
  <Sheet
227
295
  open={sheetOpen}