@hed-hog/billing 0.0.2

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 (232) hide show
  1. package/dist/adapters/billing-gateway-registry.d.ts +8 -0
  2. package/dist/adapters/billing-gateway-registry.d.ts.map +1 -0
  3. package/dist/adapters/billing-gateway-registry.js +33 -0
  4. package/dist/adapters/billing-gateway-registry.js.map +1 -0
  5. package/dist/adapters/mercadopago.adapter.d.ts +44 -0
  6. package/dist/adapters/mercadopago.adapter.d.ts.map +1 -0
  7. package/dist/adapters/mercadopago.adapter.js +68 -0
  8. package/dist/adapters/mercadopago.adapter.js.map +1 -0
  9. package/dist/adapters/pagarme.adapter.d.ts +44 -0
  10. package/dist/adapters/pagarme.adapter.d.ts.map +1 -0
  11. package/dist/adapters/pagarme.adapter.js +68 -0
  12. package/dist/adapters/pagarme.adapter.js.map +1 -0
  13. package/dist/adapters/payment-gateway.adapter.d.ts +15 -0
  14. package/dist/adapters/payment-gateway.adapter.d.ts.map +1 -0
  15. package/dist/adapters/payment-gateway.adapter.js +3 -0
  16. package/dist/adapters/payment-gateway.adapter.js.map +1 -0
  17. package/dist/adapters/stripe.adapter.d.ts +44 -0
  18. package/dist/adapters/stripe.adapter.d.ts.map +1 -0
  19. package/dist/adapters/stripe.adapter.js +69 -0
  20. package/dist/adapters/stripe.adapter.js.map +1 -0
  21. package/dist/billing-contracts.controller.d.ts +22 -0
  22. package/dist/billing-contracts.controller.d.ts.map +1 -0
  23. package/dist/billing-contracts.controller.js +84 -0
  24. package/dist/billing-contracts.controller.js.map +1 -0
  25. package/dist/billing-coupons.controller.d.ts +22 -0
  26. package/dist/billing-coupons.controller.d.ts.map +1 -0
  27. package/dist/billing-coupons.controller.js +84 -0
  28. package/dist/billing-coupons.controller.js.map +1 -0
  29. package/dist/billing-dashboard.controller.d.ts +13 -0
  30. package/dist/billing-dashboard.controller.d.ts.map +1 -0
  31. package/dist/billing-dashboard.controller.js +36 -0
  32. package/dist/billing-dashboard.controller.js.map +1 -0
  33. package/dist/billing-entitlements.controller.d.ts +19 -0
  34. package/dist/billing-entitlements.controller.d.ts.map +1 -0
  35. package/dist/billing-entitlements.controller.js +62 -0
  36. package/dist/billing-entitlements.controller.js.map +1 -0
  37. package/dist/billing-gateways.controller.d.ts +8 -0
  38. package/dist/billing-gateways.controller.d.ts.map +1 -0
  39. package/dist/billing-gateways.controller.js +50 -0
  40. package/dist/billing-gateways.controller.js.map +1 -0
  41. package/dist/billing-invoices.controller.d.ts +18 -0
  42. package/dist/billing-invoices.controller.d.ts.map +1 -0
  43. package/dist/billing-invoices.controller.js +61 -0
  44. package/dist/billing-invoices.controller.js.map +1 -0
  45. package/dist/billing-offers.controller.d.ts +22 -0
  46. package/dist/billing-offers.controller.d.ts.map +1 -0
  47. package/dist/billing-offers.controller.js +84 -0
  48. package/dist/billing-offers.controller.js.map +1 -0
  49. package/dist/billing-orders.controller.d.ts +19 -0
  50. package/dist/billing-orders.controller.d.ts.map +1 -0
  51. package/dist/billing-orders.controller.js +62 -0
  52. package/dist/billing-orders.controller.js.map +1 -0
  53. package/dist/billing-payments.controller.d.ts +17 -0
  54. package/dist/billing-payments.controller.d.ts.map +1 -0
  55. package/dist/billing-payments.controller.js +51 -0
  56. package/dist/billing-payments.controller.js.map +1 -0
  57. package/dist/billing-prices.controller.d.ts +22 -0
  58. package/dist/billing-prices.controller.d.ts.map +1 -0
  59. package/dist/billing-prices.controller.js +84 -0
  60. package/dist/billing-prices.controller.js.map +1 -0
  61. package/dist/billing-products.controller.d.ts +22 -0
  62. package/dist/billing-products.controller.d.ts.map +1 -0
  63. package/dist/billing-products.controller.js +84 -0
  64. package/dist/billing-products.controller.js.map +1 -0
  65. package/dist/billing-refunds.controller.d.ts +16 -0
  66. package/dist/billing-refunds.controller.d.ts.map +1 -0
  67. package/dist/billing-refunds.controller.js +41 -0
  68. package/dist/billing-refunds.controller.js.map +1 -0
  69. package/dist/billing-subscriptions.controller.d.ts +22 -0
  70. package/dist/billing-subscriptions.controller.d.ts.map +1 -0
  71. package/dist/billing-subscriptions.controller.js +92 -0
  72. package/dist/billing-subscriptions.controller.js.map +1 -0
  73. package/dist/billing-webhooks.controller.d.ts +16 -0
  74. package/dist/billing-webhooks.controller.d.ts.map +1 -0
  75. package/dist/billing-webhooks.controller.js +41 -0
  76. package/dist/billing-webhooks.controller.js.map +1 -0
  77. package/dist/billing.module.d.ts +3 -0
  78. package/dist/billing.module.d.ts.map +1 -0
  79. package/dist/billing.module.js +61 -0
  80. package/dist/billing.module.js.map +1 -0
  81. package/dist/billing.service.d.ts +169 -0
  82. package/dist/billing.service.d.ts.map +1 -0
  83. package/dist/billing.service.js +290 -0
  84. package/dist/billing.service.js.map +1 -0
  85. package/dist/dto/create-contract.dto.d.ts +11 -0
  86. package/dist/dto/create-contract.dto.d.ts.map +1 -0
  87. package/dist/dto/create-contract.dto.js +52 -0
  88. package/dist/dto/create-contract.dto.js.map +1 -0
  89. package/dist/dto/create-coupon.dto.d.ts +12 -0
  90. package/dist/dto/create-coupon.dto.d.ts.map +1 -0
  91. package/dist/dto/create-coupon.dto.js +60 -0
  92. package/dist/dto/create-coupon.dto.js.map +1 -0
  93. package/dist/dto/create-entitlement.dto.d.ts +12 -0
  94. package/dist/dto/create-entitlement.dto.d.ts.map +1 -0
  95. package/dist/dto/create-entitlement.dto.js +54 -0
  96. package/dist/dto/create-entitlement.dto.js.map +1 -0
  97. package/dist/dto/create-offer.dto.d.ts +7 -0
  98. package/dist/dto/create-offer.dto.d.ts.map +1 -0
  99. package/dist/dto/create-offer.dto.js +35 -0
  100. package/dist/dto/create-offer.dto.js.map +1 -0
  101. package/dist/dto/create-order.dto.d.ts +12 -0
  102. package/dist/dto/create-order.dto.d.ts.map +1 -0
  103. package/dist/dto/create-order.dto.js +65 -0
  104. package/dist/dto/create-order.dto.js.map +1 -0
  105. package/dist/dto/create-price.dto.d.ts +15 -0
  106. package/dist/dto/create-price.dto.d.ts.map +1 -0
  107. package/dist/dto/create-price.dto.js +76 -0
  108. package/dist/dto/create-price.dto.js.map +1 -0
  109. package/dist/dto/create-product.dto.d.ts +9 -0
  110. package/dist/dto/create-product.dto.d.ts.map +1 -0
  111. package/dist/dto/create-product.dto.js +45 -0
  112. package/dist/dto/create-product.dto.js.map +1 -0
  113. package/dist/dto/create-subscription.dto.d.ts +14 -0
  114. package/dist/dto/create-subscription.dto.d.ts.map +1 -0
  115. package/dist/dto/create-subscription.dto.js +67 -0
  116. package/dist/dto/create-subscription.dto.js.map +1 -0
  117. package/dist/dto/update-contract.dto.d.ts +6 -0
  118. package/dist/dto/update-contract.dto.d.ts.map +1 -0
  119. package/dist/dto/update-contract.dto.js +9 -0
  120. package/dist/dto/update-contract.dto.js.map +1 -0
  121. package/dist/dto/update-coupon.dto.d.ts +6 -0
  122. package/dist/dto/update-coupon.dto.d.ts.map +1 -0
  123. package/dist/dto/update-coupon.dto.js +9 -0
  124. package/dist/dto/update-coupon.dto.js.map +1 -0
  125. package/dist/dto/update-offer.dto.d.ts +6 -0
  126. package/dist/dto/update-offer.dto.d.ts.map +1 -0
  127. package/dist/dto/update-offer.dto.js +9 -0
  128. package/dist/dto/update-offer.dto.js.map +1 -0
  129. package/dist/dto/update-order.dto.d.ts +6 -0
  130. package/dist/dto/update-order.dto.d.ts.map +1 -0
  131. package/dist/dto/update-order.dto.js +9 -0
  132. package/dist/dto/update-order.dto.js.map +1 -0
  133. package/dist/dto/update-price.dto.d.ts +6 -0
  134. package/dist/dto/update-price.dto.d.ts.map +1 -0
  135. package/dist/dto/update-price.dto.js +9 -0
  136. package/dist/dto/update-price.dto.js.map +1 -0
  137. package/dist/dto/update-product.dto.d.ts +6 -0
  138. package/dist/dto/update-product.dto.d.ts.map +1 -0
  139. package/dist/dto/update-product.dto.js +9 -0
  140. package/dist/dto/update-product.dto.js.map +1 -0
  141. package/dist/dto/update-subscription.dto.d.ts +6 -0
  142. package/dist/dto/update-subscription.dto.d.ts.map +1 -0
  143. package/dist/dto/update-subscription.dto.js +9 -0
  144. package/dist/dto/update-subscription.dto.js.map +1 -0
  145. package/dist/index.d.ts +32 -0
  146. package/dist/index.d.ts.map +1 -0
  147. package/dist/index.js +48 -0
  148. package/dist/index.js.map +1 -0
  149. package/hedhog/data/menu.yaml +284 -0
  150. package/hedhog/data/role.yaml +7 -0
  151. package/hedhog/data/route.yaml +422 -0
  152. package/hedhog/frontend/app/_lib/billing-mocks.ts.ejs +270 -0
  153. package/hedhog/frontend/app/contracts/page.tsx.ejs +562 -0
  154. package/hedhog/frontend/app/coupons/page.tsx.ejs +669 -0
  155. package/hedhog/frontend/app/entitlements/page.tsx.ejs +526 -0
  156. package/hedhog/frontend/app/gateways/page.tsx.ejs +308 -0
  157. package/hedhog/frontend/app/invoices/page.tsx.ejs +179 -0
  158. package/hedhog/frontend/app/offers/page.tsx.ejs +483 -0
  159. package/hedhog/frontend/app/orders/page.tsx.ejs +424 -0
  160. package/hedhog/frontend/app/page.tsx.ejs +186 -0
  161. package/hedhog/frontend/app/payments/page.tsx.ejs +187 -0
  162. package/hedhog/frontend/app/prices/page.tsx.ejs +704 -0
  163. package/hedhog/frontend/app/products/page.tsx.ejs +568 -0
  164. package/hedhog/frontend/app/refunds/page.tsx.ejs +174 -0
  165. package/hedhog/frontend/app/reports/page.tsx.ejs +177 -0
  166. package/hedhog/frontend/app/subscriptions/page.tsx.ejs +283 -0
  167. package/hedhog/frontend/app/webhooks/page.tsx.ejs +172 -0
  168. package/hedhog/frontend/messages/en.json +551 -0
  169. package/hedhog/frontend/messages/pt.json +563 -0
  170. package/hedhog/table/billing_contract.yaml +37 -0
  171. package/hedhog/table/billing_contract_seat.yaml +28 -0
  172. package/hedhog/table/billing_coupon.yaml +42 -0
  173. package/hedhog/table/billing_entitlement.yaml +41 -0
  174. package/hedhog/table/billing_entitlement_event.yaml +20 -0
  175. package/hedhog/table/billing_invoice.yaml +60 -0
  176. package/hedhog/table/billing_invoice_item.yaml +25 -0
  177. package/hedhog/table/billing_offer.yaml +22 -0
  178. package/hedhog/table/billing_offer_price.yaml +23 -0
  179. package/hedhog/table/billing_order.yaml +50 -0
  180. package/hedhog/table/billing_order_item.yaml +35 -0
  181. package/hedhog/table/billing_payment.yaml +66 -0
  182. package/hedhog/table/billing_payment_method.yaml +49 -0
  183. package/hedhog/table/billing_payment_provider.yaml +21 -0
  184. package/hedhog/table/billing_price.yaml +53 -0
  185. package/hedhog/table/billing_product.yaml +31 -0
  186. package/hedhog/table/billing_product_item.yaml +24 -0
  187. package/hedhog/table/billing_provider_event.yaml +31 -0
  188. package/hedhog/table/billing_refund.yaml +41 -0
  189. package/hedhog/table/billing_seat_allocation.yaml +42 -0
  190. package/hedhog/table/billing_subscription.yaml +66 -0
  191. package/hedhog/table/billing_subscription_event.yaml +20 -0
  192. package/hedhog/table/billing_subscription_item.yaml +29 -0
  193. package/package.json +37 -0
  194. package/src/adapters/billing-gateway-registry.ts +23 -0
  195. package/src/adapters/mercadopago.adapter.ts +66 -0
  196. package/src/adapters/pagarme.adapter.ts +66 -0
  197. package/src/adapters/payment-gateway.adapter.ts +14 -0
  198. package/src/adapters/stripe.adapter.ts +67 -0
  199. package/src/billing-contracts.controller.ts +46 -0
  200. package/src/billing-coupons.controller.ts +46 -0
  201. package/src/billing-dashboard.controller.ts +14 -0
  202. package/src/billing-entitlements.controller.ts +34 -0
  203. package/src/billing-gateways.controller.ts +19 -0
  204. package/src/billing-invoices.controller.ts +32 -0
  205. package/src/billing-offers.controller.ts +46 -0
  206. package/src/billing-orders.controller.ts +33 -0
  207. package/src/billing-payments.controller.ts +20 -0
  208. package/src/billing-prices.controller.ts +46 -0
  209. package/src/billing-products.controller.ts +46 -0
  210. package/src/billing-refunds.controller.ts +15 -0
  211. package/src/billing-subscriptions.controller.ts +49 -0
  212. package/src/billing-webhooks.controller.ts +15 -0
  213. package/src/billing.module.ts +48 -0
  214. package/src/billing.service.ts +391 -0
  215. package/src/dto/create-contract.dto.ts +30 -0
  216. package/src/dto/create-coupon.dto.ts +37 -0
  217. package/src/dto/create-entitlement.dto.ts +31 -0
  218. package/src/dto/create-offer.dto.ts +17 -0
  219. package/src/dto/create-order.dto.ts +42 -0
  220. package/src/dto/create-price.dto.ts +50 -0
  221. package/src/dto/create-product.dto.ts +25 -0
  222. package/src/dto/create-subscription.dto.ts +42 -0
  223. package/src/dto/update-contract.dto.ts +4 -0
  224. package/src/dto/update-coupon.dto.ts +4 -0
  225. package/src/dto/update-offer.dto.ts +4 -0
  226. package/src/dto/update-order.dto.ts +4 -0
  227. package/src/dto/update-price.dto.ts +4 -0
  228. package/src/dto/update-product.dto.ts +4 -0
  229. package/src/dto/update-subscription.dto.ts +4 -0
  230. package/src/index.ts +32 -0
  231. package/src/language/en.json +8 -0
  232. package/src/language/pt.json +8 -0
@@ -0,0 +1,704 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Page,
5
+ PageHeader,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ } from '@/components/entity-list';
9
+ import {
10
+ AlertDialog,
11
+ AlertDialogAction,
12
+ AlertDialogCancel,
13
+ AlertDialogContent,
14
+ AlertDialogDescription,
15
+ AlertDialogFooter,
16
+ AlertDialogHeader,
17
+ AlertDialogTitle,
18
+ } from '@/components/ui/alert-dialog';
19
+ import { Badge } from '@/components/ui/badge';
20
+ import { Button } from '@/components/ui/button';
21
+ import {
22
+ Form,
23
+ FormControl,
24
+ FormField,
25
+ FormItem,
26
+ FormLabel,
27
+ FormMessage,
28
+ } from '@/components/ui/form';
29
+ import { Input } from '@/components/ui/input';
30
+ import { InputMoney } from '@/components/ui/input-money';
31
+ import {
32
+ Select,
33
+ SelectContent,
34
+ SelectItem,
35
+ SelectTrigger,
36
+ SelectValue,
37
+ } from '@/components/ui/select';
38
+ import {
39
+ Sheet,
40
+ SheetContent,
41
+ SheetDescription,
42
+ SheetHeader,
43
+ SheetTitle,
44
+ } from '@/components/ui/sheet';
45
+ import { Switch } from '@/components/ui/switch';
46
+ import {
47
+ Table,
48
+ TableBody,
49
+ TableCell,
50
+ TableHead,
51
+ TableHeader,
52
+ TableRow,
53
+ } from '@/components/ui/table';
54
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
55
+ import { zodResolver } from '@hookform/resolvers/zod';
56
+ import { Pencil, Plus, Trash2 } from 'lucide-react';
57
+ import { useTranslations } from 'next-intl';
58
+ import { useEffect, useState } from 'react';
59
+ import { useForm } from 'react-hook-form';
60
+ import { z } from 'zod';
61
+
62
+ type Product = {
63
+ id: number;
64
+ name: string;
65
+ };
66
+
67
+ type Price = {
68
+ id: number;
69
+ name: string;
70
+ product_id: number;
71
+ product_name?: string;
72
+ product?: { name?: string };
73
+ billing_type: string;
74
+ currency: string;
75
+ amount_cents: number;
76
+ interval_unit?: string | null;
77
+ interval_count?: number | null;
78
+ trial_days?: number | null;
79
+ is_active: boolean;
80
+ };
81
+
82
+ type ListResponse<T> = {
83
+ data?: T[];
84
+ total?: number;
85
+ };
86
+
87
+ const formatCurrency = (cents: number, currency = 'BRL') =>
88
+ new Intl.NumberFormat('pt-BR', { style: 'currency', currency }).format(
89
+ cents / 100
90
+ );
91
+
92
+ const badgeClass = (value: string) => {
93
+ if (['active', 'paid', 'authorized', 'approved'].includes(value)) {
94
+ return 'bg-green-100 text-green-800';
95
+ }
96
+ if (['pending', 'open', 'draft'].includes(value)) {
97
+ return 'bg-yellow-100 text-yellow-800';
98
+ }
99
+ if (['failed', 'canceled', 'void'].includes(value)) {
100
+ return 'bg-red-100 text-red-800';
101
+ }
102
+ if (['paused', 'inactive'].includes(value)) {
103
+ return 'bg-gray-100 text-gray-800';
104
+ }
105
+ return 'bg-slate-100 text-slate-800';
106
+ };
107
+
108
+ const priceSchema = z.object({
109
+ name: z.string().trim().min(1),
110
+ product_id: z.coerce.number().min(1),
111
+ billing_type: z.enum(['one_time', 'recurring', 'usage']),
112
+ currency: z.string().trim().default('BRL'),
113
+ amount_cents: z.coerce.number().min(0),
114
+ interval_unit: z.enum(['day', 'week', 'month', 'year']).optional(),
115
+ interval_count: z.coerce.number().min(1).optional(),
116
+ trial_days: z.coerce.number().min(0).optional(),
117
+ is_active: z.boolean(),
118
+ });
119
+
120
+ type PriceFormValues = z.infer<typeof priceSchema>;
121
+
122
+ const mapBillingTypeToApi = (value: PriceFormValues['billing_type']) => {
123
+ if (value === 'usage') {
124
+ return 'recurring';
125
+ }
126
+
127
+ return value;
128
+ };
129
+
130
+ export default function BillingPricesPage() {
131
+ const t = useTranslations('BillingPricesPage');
132
+ const { request, showToastHandler, currentLocaleCode } = useApp();
133
+ const [search, setSearch] = useState('');
134
+ const [billingFilter, setBillingFilter] = useState('all');
135
+ const [page, setPage] = useState(1);
136
+ const [pageSize, setPageSize] = useState(12);
137
+ const [sheetOpen, setSheetOpen] = useState(false);
138
+ const [editingPrice, setEditingPrice] = useState<Price | null>(null);
139
+ const [deleteId, setDeleteId] = useState<number | null>(null);
140
+
141
+ const form = useForm<PriceFormValues>({
142
+ resolver: zodResolver(priceSchema),
143
+ defaultValues: {
144
+ name: '',
145
+ product_id: 0,
146
+ billing_type: 'recurring',
147
+ currency: 'BRL',
148
+ amount_cents: 0,
149
+ interval_unit: 'month',
150
+ interval_count: 1,
151
+ trial_days: 0,
152
+ is_active: true,
153
+ },
154
+ });
155
+
156
+ const { data: products } = useQuery<Product[]>({
157
+ queryKey: ['billing-products-select', currentLocaleCode],
158
+ queryFn: async () => {
159
+ const response = await request<ListResponse<Product>>({
160
+ url: '/billing/products',
161
+ method: 'GET',
162
+ params: { page: 1, pageSize: 200 },
163
+ });
164
+
165
+ const payload = response.data as ListResponse<Product> | Product[];
166
+ if (Array.isArray(payload)) {
167
+ return payload;
168
+ }
169
+
170
+ return payload.data ?? [];
171
+ },
172
+ placeholderData: [],
173
+ });
174
+
175
+ useEffect(() => {
176
+ if (!sheetOpen) {
177
+ return;
178
+ }
179
+
180
+ if (editingPrice) {
181
+ form.reset({
182
+ name: editingPrice.name,
183
+ product_id: editingPrice.product_id,
184
+ billing_type:
185
+ editingPrice.billing_type as PriceFormValues['billing_type'],
186
+ currency: editingPrice.currency,
187
+ amount_cents: editingPrice.amount_cents,
188
+ interval_unit: (editingPrice.interval_unit ?? 'month') as
189
+ | 'day'
190
+ | 'week'
191
+ | 'month'
192
+ | 'year',
193
+ interval_count: editingPrice.interval_count ?? 1,
194
+ trial_days: editingPrice.trial_days ?? 0,
195
+ is_active: Boolean(editingPrice.is_active),
196
+ });
197
+ return;
198
+ }
199
+
200
+ form.reset({
201
+ name: '',
202
+ product_id: 0,
203
+ billing_type: 'recurring',
204
+ currency: 'BRL',
205
+ amount_cents: 0,
206
+ interval_unit: 'month',
207
+ interval_count: 1,
208
+ trial_days: 0,
209
+ is_active: true,
210
+ });
211
+ }, [editingPrice, form, sheetOpen]);
212
+
213
+ const { data, refetch } = useQuery<{ items: Price[]; total: number }>({
214
+ queryKey: [
215
+ 'billing-prices',
216
+ currentLocaleCode,
217
+ search,
218
+ billingFilter,
219
+ page,
220
+ pageSize,
221
+ ],
222
+ queryFn: async () => {
223
+ const response = await request<ListResponse<Price>>({
224
+ url: '/billing/prices',
225
+ method: 'GET',
226
+ params: {
227
+ page,
228
+ pageSize,
229
+ search: search.trim() || undefined,
230
+ billing_type: billingFilter !== 'all' ? billingFilter : undefined,
231
+ },
232
+ });
233
+
234
+ const payload = response.data as ListResponse<Price> | Price[];
235
+ if (Array.isArray(payload)) {
236
+ return { items: payload, total: payload.length };
237
+ }
238
+
239
+ return {
240
+ items: payload.data ?? [],
241
+ total: payload.total ?? payload.data?.length ?? 0,
242
+ };
243
+ },
244
+ placeholderData: (old) => old,
245
+ });
246
+
247
+ const items = data?.items ?? [];
248
+ const totalItems = data?.total ?? 0;
249
+
250
+ const handleSubmit = async (values: PriceFormValues) => {
251
+ try {
252
+ const payload = {
253
+ ...values,
254
+ billing_type: mapBillingTypeToApi(values.billing_type),
255
+ interval_unit:
256
+ values.billing_type === 'recurring'
257
+ ? values.interval_unit
258
+ : undefined,
259
+ interval_count:
260
+ values.billing_type === 'recurring'
261
+ ? values.interval_count
262
+ : undefined,
263
+ trial_days:
264
+ values.billing_type === 'recurring' ? values.trial_days : undefined,
265
+ };
266
+
267
+ if (editingPrice) {
268
+ await request({
269
+ url: `/billing/prices/${editingPrice.id}`,
270
+ method: 'PATCH',
271
+ data: payload,
272
+ });
273
+ } else {
274
+ await request({
275
+ url: '/billing/prices',
276
+ method: 'POST',
277
+ data: payload,
278
+ });
279
+ }
280
+
281
+ showToastHandler?.('success', t('messages.saveSuccess'));
282
+ setSheetOpen(false);
283
+ setEditingPrice(null);
284
+ form.reset();
285
+ await refetch();
286
+ } catch {
287
+ showToastHandler?.('error', t('messages.saveError'));
288
+ }
289
+ };
290
+
291
+ const handleDelete = async () => {
292
+ if (!deleteId) {
293
+ return;
294
+ }
295
+
296
+ try {
297
+ await request({
298
+ url: `/billing/prices/${deleteId}`,
299
+ method: 'DELETE',
300
+ });
301
+ showToastHandler?.('success', t('messages.deleteSuccess'));
302
+ setDeleteId(null);
303
+ await refetch();
304
+ } catch {
305
+ showToastHandler?.('error', t('messages.deleteError'));
306
+ }
307
+ };
308
+
309
+ return (
310
+ <Page>
311
+ <PageHeader
312
+ title={t('title')}
313
+ description={t('description')}
314
+ breadcrumbs={[
315
+ { label: t('breadcrumbs.home'), href: '/' },
316
+ { label: t('breadcrumbs.billing'), href: '/billing' },
317
+ { label: t('breadcrumbs.prices') },
318
+ ]}
319
+ actions={[
320
+ {
321
+ label: t('actions.create'),
322
+ onClick: () => {
323
+ setEditingPrice(null);
324
+ setSheetOpen(true);
325
+ },
326
+ icon: <Plus className="size-4" />,
327
+ },
328
+ ]}
329
+ />
330
+
331
+ <SearchBar
332
+ searchQuery={search}
333
+ onSearchChange={(value) => {
334
+ setSearch(value);
335
+ setPage(1);
336
+ }}
337
+ onSearch={() => {
338
+ setPage(1);
339
+ void refetch();
340
+ }}
341
+ placeholder={t('filters.searchPlaceholder')}
342
+ filters={{
343
+ value: billingFilter,
344
+ options: [
345
+ { label: t('filters.all'), value: 'all' },
346
+ { label: 'one_time', value: 'one_time' },
347
+ { label: 'recurring', value: 'recurring' },
348
+ { label: 'usage', value: 'usage' },
349
+ ],
350
+ onChange: (value) => {
351
+ setBillingFilter(value);
352
+ setPage(1);
353
+ },
354
+ placeholder: t('filters.billingType'),
355
+ }}
356
+ />
357
+
358
+ <div className="overflow-x-auto rounded-md border">
359
+ <Table>
360
+ <TableHeader>
361
+ <TableRow>
362
+ <TableHead>{t('table.columns.name')}</TableHead>
363
+ <TableHead>{t('table.columns.product')}</TableHead>
364
+ <TableHead>{t('table.columns.billingType')}</TableHead>
365
+ <TableHead>{t('table.columns.amount')}</TableHead>
366
+ <TableHead>{t('table.columns.intervalUnit')}</TableHead>
367
+ <TableHead>{t('table.columns.isActive')}</TableHead>
368
+ <TableHead className="w-[120px] text-right">
369
+ {t('table.columns.actions')}
370
+ </TableHead>
371
+ </TableRow>
372
+ </TableHeader>
373
+ <TableBody>
374
+ {items.length === 0 && (
375
+ <TableRow>
376
+ <TableCell
377
+ colSpan={7}
378
+ className="text-center text-muted-foreground"
379
+ >
380
+ {t('table.empty')}
381
+ </TableCell>
382
+ </TableRow>
383
+ )}
384
+ {items.map((item) => (
385
+ <TableRow key={item.id}>
386
+ <TableCell className="font-medium">{item.name}</TableCell>
387
+ <TableCell>
388
+ {item.product_name ?? item.product?.name ?? '-'}
389
+ </TableCell>
390
+ <TableCell>
391
+ <Badge className={badgeClass(item.billing_type)}>
392
+ {item.billing_type}
393
+ </Badge>
394
+ </TableCell>
395
+ <TableCell>
396
+ {formatCurrency(item.amount_cents, item.currency)}
397
+ </TableCell>
398
+ <TableCell>{item.interval_unit ?? '-'}</TableCell>
399
+ <TableCell>
400
+ <Badge
401
+ className={badgeClass(
402
+ item.is_active ? 'active' : 'inactive'
403
+ )}
404
+ >
405
+ {item.is_active ? t('status.active') : t('status.inactive')}
406
+ </Badge>
407
+ </TableCell>
408
+ <TableCell>
409
+ <div className="flex justify-end gap-2">
410
+ <Button
411
+ variant="outline"
412
+ size="icon"
413
+ onClick={() => {
414
+ setEditingPrice(item);
415
+ setSheetOpen(true);
416
+ }}
417
+ >
418
+ <Pencil className="size-4" />
419
+ </Button>
420
+ <Button
421
+ variant="destructive"
422
+ size="icon"
423
+ onClick={() => setDeleteId(item.id)}
424
+ >
425
+ <Trash2 className="size-4" />
426
+ </Button>
427
+ </div>
428
+ </TableCell>
429
+ </TableRow>
430
+ ))}
431
+ </TableBody>
432
+ </Table>
433
+ </div>
434
+
435
+ <PaginationFooter
436
+ currentPage={page}
437
+ pageSize={pageSize}
438
+ totalItems={totalItems}
439
+ onPageChange={setPage}
440
+ onPageSizeChange={(nextSize) => {
441
+ setPageSize(nextSize);
442
+ setPage(1);
443
+ }}
444
+ />
445
+
446
+ <Sheet
447
+ open={sheetOpen}
448
+ onOpenChange={(open) => {
449
+ setSheetOpen(open);
450
+ if (!open) {
451
+ setEditingPrice(null);
452
+ }
453
+ }}
454
+ >
455
+ <SheetContent className="w-full sm:max-w-xl">
456
+ <SheetHeader>
457
+ <SheetTitle>
458
+ {editingPrice ? t('sheet.editTitle') : t('sheet.createTitle')}
459
+ </SheetTitle>
460
+ <SheetDescription>{t('sheet.description')}</SheetDescription>
461
+ </SheetHeader>
462
+
463
+ <Form {...form}>
464
+ <form
465
+ onSubmit={form.handleSubmit(handleSubmit)}
466
+ className="space-y-4 p-4"
467
+ >
468
+ <FormField
469
+ control={form.control}
470
+ name="name"
471
+ render={({ field }) => (
472
+ <FormItem>
473
+ <FormLabel>{t('form.name')}</FormLabel>
474
+ <FormControl>
475
+ <Input
476
+ placeholder={t('form.namePlaceholder')}
477
+ {...field}
478
+ />
479
+ </FormControl>
480
+ <FormMessage />
481
+ </FormItem>
482
+ )}
483
+ />
484
+
485
+ <FormField
486
+ control={form.control}
487
+ name="product_id"
488
+ render={({ field }) => (
489
+ <FormItem>
490
+ <FormLabel>{t('form.product')}</FormLabel>
491
+ <Select
492
+ value={field.value ? String(field.value) : undefined}
493
+ onValueChange={(value) => field.onChange(Number(value))}
494
+ >
495
+ <FormControl>
496
+ <SelectTrigger className="w-full">
497
+ <SelectValue
498
+ placeholder={t('form.productPlaceholder')}
499
+ />
500
+ </SelectTrigger>
501
+ </FormControl>
502
+ <SelectContent>
503
+ {(products ?? []).map((product) => (
504
+ <SelectItem
505
+ key={product.id}
506
+ value={String(product.id)}
507
+ >
508
+ {product.name}
509
+ </SelectItem>
510
+ ))}
511
+ </SelectContent>
512
+ </Select>
513
+ <FormMessage />
514
+ </FormItem>
515
+ )}
516
+ />
517
+
518
+ <div className="grid grid-cols-2 gap-4">
519
+ <FormField
520
+ control={form.control}
521
+ name="billing_type"
522
+ render={({ field }) => (
523
+ <FormItem>
524
+ <FormLabel>{t('form.billingType')}</FormLabel>
525
+ <Select
526
+ value={field.value}
527
+ onValueChange={field.onChange}
528
+ >
529
+ <FormControl>
530
+ <SelectTrigger className="w-full">
531
+ <SelectValue />
532
+ </SelectTrigger>
533
+ </FormControl>
534
+ <SelectContent>
535
+ <SelectItem value="one_time">one_time</SelectItem>
536
+ <SelectItem value="recurring">recurring</SelectItem>
537
+ <SelectItem value="usage">usage</SelectItem>
538
+ </SelectContent>
539
+ </Select>
540
+ <FormMessage />
541
+ </FormItem>
542
+ )}
543
+ />
544
+
545
+ <FormField
546
+ control={form.control}
547
+ name="currency"
548
+ render={({ field }) => (
549
+ <FormItem>
550
+ <FormLabel>{t('form.currency')}</FormLabel>
551
+ <FormControl>
552
+ <Input
553
+ placeholder="BRL"
554
+ {...field}
555
+ value={field.value ?? 'BRL'}
556
+ />
557
+ </FormControl>
558
+ <FormMessage />
559
+ </FormItem>
560
+ )}
561
+ />
562
+ </div>
563
+
564
+ <FormField
565
+ control={form.control}
566
+ name="amount_cents"
567
+ render={({ field }) => (
568
+ <FormItem>
569
+ <FormLabel>{t('form.amount')}</FormLabel>
570
+ <FormControl>
571
+ <InputMoney
572
+ value={(field.value ?? 0) / 100}
573
+ onValueChange={(value) =>
574
+ field.onChange(Math.round((value ?? 0) * 100))
575
+ }
576
+ />
577
+ </FormControl>
578
+ <FormMessage />
579
+ </FormItem>
580
+ )}
581
+ />
582
+
583
+ <div className="grid grid-cols-3 gap-4">
584
+ <FormField
585
+ control={form.control}
586
+ name="interval_unit"
587
+ render={({ field }) => (
588
+ <FormItem>
589
+ <FormLabel>{t('form.intervalUnit')}</FormLabel>
590
+ <Select
591
+ value={field.value}
592
+ onValueChange={field.onChange}
593
+ >
594
+ <FormControl>
595
+ <SelectTrigger className="w-full">
596
+ <SelectValue />
597
+ </SelectTrigger>
598
+ </FormControl>
599
+ <SelectContent>
600
+ <SelectItem value="day">day</SelectItem>
601
+ <SelectItem value="week">week</SelectItem>
602
+ <SelectItem value="month">month</SelectItem>
603
+ <SelectItem value="year">year</SelectItem>
604
+ </SelectContent>
605
+ </Select>
606
+ <FormMessage />
607
+ </FormItem>
608
+ )}
609
+ />
610
+ <FormField
611
+ control={form.control}
612
+ name="interval_count"
613
+ render={({ field }) => (
614
+ <FormItem>
615
+ <FormLabel>{t('form.intervalCount')}</FormLabel>
616
+ <FormControl>
617
+ <Input
618
+ type="number"
619
+ min={1}
620
+ {...field}
621
+ value={field.value ?? 1}
622
+ onChange={(event) =>
623
+ field.onChange(Number(event.target.value))
624
+ }
625
+ />
626
+ </FormControl>
627
+ <FormMessage />
628
+ </FormItem>
629
+ )}
630
+ />
631
+ <FormField
632
+ control={form.control}
633
+ name="trial_days"
634
+ render={({ field }) => (
635
+ <FormItem>
636
+ <FormLabel>{t('form.trialDays')}</FormLabel>
637
+ <FormControl>
638
+ <Input
639
+ type="number"
640
+ min={0}
641
+ {...field}
642
+ value={field.value ?? 0}
643
+ onChange={(event) =>
644
+ field.onChange(Number(event.target.value))
645
+ }
646
+ />
647
+ </FormControl>
648
+ <FormMessage />
649
+ </FormItem>
650
+ )}
651
+ />
652
+ </div>
653
+
654
+ <FormField
655
+ control={form.control}
656
+ name="is_active"
657
+ render={({ field }) => (
658
+ <FormItem className="flex items-center justify-between">
659
+ <FormLabel>{t('form.isActive')}</FormLabel>
660
+ <FormControl>
661
+ <Switch
662
+ checked={field.value}
663
+ onCheckedChange={field.onChange}
664
+ />
665
+ </FormControl>
666
+ <FormMessage />
667
+ </FormItem>
668
+ )}
669
+ />
670
+
671
+ <Button
672
+ type="submit"
673
+ className="w-full"
674
+ disabled={form.formState.isSubmitting}
675
+ >
676
+ {t('actions.save')}
677
+ </Button>
678
+ </form>
679
+ </Form>
680
+ </SheetContent>
681
+ </Sheet>
682
+
683
+ <AlertDialog
684
+ open={deleteId !== null}
685
+ onOpenChange={(open) => !open && setDeleteId(null)}
686
+ >
687
+ <AlertDialogContent>
688
+ <AlertDialogHeader>
689
+ <AlertDialogTitle>{t('deleteDialog.title')}</AlertDialogTitle>
690
+ <AlertDialogDescription>
691
+ {t('deleteDialog.description')}
692
+ </AlertDialogDescription>
693
+ </AlertDialogHeader>
694
+ <AlertDialogFooter>
695
+ <AlertDialogCancel>{t('deleteDialog.cancel')}</AlertDialogCancel>
696
+ <AlertDialogAction onClick={handleDelete}>
697
+ {t('deleteDialog.confirm')}
698
+ </AlertDialogAction>
699
+ </AlertDialogFooter>
700
+ </AlertDialogContent>
701
+ </AlertDialog>
702
+ </Page>
703
+ );
704
+ }