@kernhq/module-billing 0.5.10 → 0.5.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernhq/module-billing",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
4
4
  "description": "Kern billing module: plans, entitlements, subscriptions and Stripe — the mechanism that lets any Kern instance sell seats on itself",
5
5
  "homepage": "https://github.com/KernAIO/module-billing#readme",
6
6
  "license": "AGPL-3.0-only",
@@ -204,15 +204,17 @@ const planOptions = $derived([
204
204
  {:else}
205
205
  <!-- the table scrolls inside itself; the page body must never scroll sideways -->
206
206
  <div class="overflow-x-auto">
207
+ <!-- Seven columns, sized to fit the admin pane at a laptop width: this sat at eight and was
208
+ wider than the pane behind the second sidebar, so the revenue column and the row menu
209
+ lived off-screen with nothing to say so. Seats and storage share a cell. -->
207
210
  <Table
208
- columns="minmax(150px,2fr) minmax(110px,1fr) minmax(104px,auto) minmax(68px,auto) minmax(84px,auto) minmax(96px,auto) minmax(84px,auto) 40px"
211
+ columns="minmax(100px,2fr) minmax(90px,1.2fr) minmax(104px,auto) minmax(112px,auto) minmax(80px,auto) minmax(72px,auto) 36px"
209
212
  >
210
213
  <TableHeader>
211
214
  <TableCell header>{t('admin_col_workspace')}</TableCell>
212
215
  <TableCell header>{t('admin_col_plan')}</TableCell>
213
216
  <TableCell header>{t('admin_col_status')}</TableCell>
214
- <TableCell header end>{t('admin_col_seats')}</TableCell>
215
- <TableCell header end>{t('admin_col_storage')}</TableCell>
217
+ <TableCell header end>{t('admin_col_usage')}</TableCell>
216
218
  <TableCell header>{t('admin_col_renews')}</TableCell>
217
219
  <TableCell header end>{t('admin_col_revenue')}</TableCell>
218
220
  <TableCell header end></TableCell>
@@ -227,13 +229,15 @@ const planOptions = $derived([
227
229
  </span>
228
230
  </div>
229
231
  </TableCell>
230
- <TableCell>
231
- <div class="flex items-center gap-1.5">
232
+ <!-- The badge sits under the name, never beside it: side by side, one of the two was
233
+ always the thing that got clipped ("Overrid", then "B…") in a column this narrow. -->
234
+ <TableCell class="min-w-0">
235
+ <div class="grid">
232
236
  <span class="truncate">{row.planName ?? t('no_plan')}</span>
233
237
  {#if row.overridden}
234
- <Badge tone="purple" title={t('admin_overridden_hint')}>
235
- {t('admin_overridden')}
236
- </Badge>
238
+ <span class="mt-0.5">
239
+ <Badge tone="purple" title={t('admin_overridden_hint')}>{t('admin_overridden')}</Badge>
240
+ </span>
237
241
  {/if}
238
242
  </div>
239
243
  </TableCell>
@@ -249,9 +253,8 @@ const planOptions = $derived([
249
253
  <TableCell end>
250
254
  {row.seatsPurchased > 0
251
255
  ? `${nf.format(row.seatsUsed)}/${nf.format(row.seatsPurchased)}`
252
- : nf.format(row.seatsUsed)}
256
+ : nf.format(row.seatsUsed)} · {formatBytes(row.storageBytes, locale)}
253
257
  </TableCell>
254
- <TableCell end>{formatBytes(row.storageBytes, locale)}</TableCell>
255
258
  <TableCell>{day(row.currentPeriodEnd ?? row.trialEndsAt)}</TableCell>
256
259
  <TableCell end>
257
260
  {row.monthlyMinor > 0 ? formatMoney(row.monthlyMinor, row.currency, locale) : '—'}
@@ -27,6 +27,7 @@ export const en: Record<string, Message> = {
27
27
  'billing.admin_col_renews': 'Renews',
28
28
  'billing.admin_col_revenue': 'Per month',
29
29
  'billing.admin_col_seats': 'Seats',
30
+ 'billing.admin_col_usage': 'Seats · storage',
30
31
  'billing.admin_col_status': 'Status',
31
32
  'billing.admin_col_storage': 'Storage',
32
33
  'billing.admin_col_workspace': 'Workspace',
@@ -129,6 +130,12 @@ export const en: Record<string, Message> = {
129
130
  'billing.invoice_number': 'Invoice',
130
131
  'billing.invoice_view': 'View',
131
132
  'billing.invoice_pdf': 'PDF',
133
+ 'billing.invoice_status': 'Status',
134
+ 'billing.invoice_status_paid': 'Paid',
135
+ 'billing.invoice_status_open': 'Awaiting payment',
136
+ 'billing.invoice_status_uncollectible': 'Unpaid',
137
+ 'billing.invoice_status_void': 'Void',
138
+ 'billing.invoice_status_draft': 'Draft',
132
139
  'billing.invoices_empty': 'No invoices yet',
133
140
  'billing.invoices_empty_hint': 'An invoice appears here after the first payment.',
134
141
  'billing.invoices_section': 'Invoices',
@@ -191,6 +198,7 @@ const ar: Record<string, Message> = {
191
198
  'billing.admin_col_renews': 'التجديد',
192
199
  'billing.admin_col_revenue': 'شهرياً',
193
200
  'billing.admin_col_seats': 'المقاعد',
201
+ 'billing.admin_col_usage': 'المقاعد · التخزين',
194
202
  'billing.admin_col_status': 'الحالة',
195
203
  'billing.admin_col_storage': 'التخزين',
196
204
  'billing.admin_col_workspace': 'مساحة العمل',
@@ -289,6 +297,12 @@ const ar: Record<string, Message> = {
289
297
  'billing.invoice_number': 'الفاتورة',
290
298
  'billing.invoice_view': 'عرض',
291
299
  'billing.invoice_pdf': 'PDF',
300
+ 'billing.invoice_status': 'الحالة',
301
+ 'billing.invoice_status_paid': 'مدفوعة',
302
+ 'billing.invoice_status_open': 'بانتظار الدفع',
303
+ 'billing.invoice_status_uncollectible': 'غير مدفوعة',
304
+ 'billing.invoice_status_void': 'ملغاة',
305
+ 'billing.invoice_status_draft': 'مسودة',
292
306
  'billing.invoices_empty': 'لا فواتير بعد',
293
307
  'billing.invoices_empty_hint': 'تظهر الفاتورة هنا بعد أول عملية دفع.',
294
308
  'billing.invoices_section': 'الفواتير',
@@ -350,6 +364,7 @@ const de: Record<string, Message> = {
350
364
  'billing.admin_col_renews': 'Verlängert am',
351
365
  'billing.admin_col_revenue': 'Pro Monat',
352
366
  'billing.admin_col_seats': 'Plätze',
367
+ 'billing.admin_col_usage': 'Plätze · Speicher',
353
368
  'billing.admin_col_status': 'Status',
354
369
  'billing.admin_col_storage': 'Speicher',
355
370
  'billing.admin_col_workspace': 'Arbeitsbereich',
@@ -456,6 +471,12 @@ const de: Record<string, Message> = {
456
471
  'billing.invoice_number': 'Rechnung',
457
472
  'billing.invoice_view': 'Ansehen',
458
473
  'billing.invoice_pdf': 'PDF',
474
+ 'billing.invoice_status': 'Status',
475
+ 'billing.invoice_status_paid': 'Bezahlt',
476
+ 'billing.invoice_status_open': 'Zahlung ausstehend',
477
+ 'billing.invoice_status_uncollectible': 'Unbezahlt',
478
+ 'billing.invoice_status_void': 'Storniert',
479
+ 'billing.invoice_status_draft': 'Entwurf',
459
480
  'billing.invoices_empty': 'Noch keine Rechnungen',
460
481
  'billing.invoices_empty_hint': 'Nach der ersten Zahlung erscheint hier eine Rechnung.',
461
482
  'billing.invoices_section': 'Rechnungen',
@@ -516,6 +537,7 @@ const fa: Record<string, Message> = {
516
537
  'billing.admin_col_renews': 'تمدید',
517
538
  'billing.admin_col_revenue': 'ماهانه',
518
539
  'billing.admin_col_seats': 'کاربران',
540
+ 'billing.admin_col_usage': 'کاربران · فضا',
519
541
  'billing.admin_col_status': 'وضعیت',
520
542
  'billing.admin_col_storage': 'فضای ذخیره‌سازی',
521
543
  'billing.admin_col_workspace': 'فضای کاری',
@@ -616,6 +638,12 @@ const fa: Record<string, Message> = {
616
638
  'billing.invoice_number': 'صورتحساب',
617
639
  'billing.invoice_view': 'نمایش',
618
640
  'billing.invoice_pdf': 'PDF',
641
+ 'billing.invoice_status': 'وضعیت',
642
+ 'billing.invoice_status_paid': 'پرداخت‌شده',
643
+ 'billing.invoice_status_open': 'در انتظار پرداخت',
644
+ 'billing.invoice_status_uncollectible': 'پرداخت‌نشده',
645
+ 'billing.invoice_status_void': 'باطل‌شده',
646
+ 'billing.invoice_status_draft': 'پیش‌نویس',
619
647
  'billing.invoices_empty': 'هنوز صورتحسابی نیست',
620
648
  'billing.invoices_empty_hint': 'پس از نخستین پرداخت، صورتحساب اینجا می‌آید.',
621
649
  'billing.invoices_section': 'صورتحساب‌ها',
@@ -673,6 +701,7 @@ const tr: Record<string, Message> = {
673
701
  'billing.admin_col_renews': 'Yenilenme',
674
702
  'billing.admin_col_revenue': 'Aylık',
675
703
  'billing.admin_col_seats': 'Kullanıcı',
704
+ 'billing.admin_col_usage': 'Kullanıcı · depolama',
676
705
  'billing.admin_col_status': 'Durum',
677
706
  'billing.admin_col_storage': 'Depolama',
678
707
  'billing.admin_col_workspace': 'Çalışma alanı',
@@ -775,6 +804,12 @@ const tr: Record<string, Message> = {
775
804
  'billing.invoice_number': 'Fatura',
776
805
  'billing.invoice_view': 'Görüntüle',
777
806
  'billing.invoice_pdf': 'PDF',
807
+ 'billing.invoice_status': 'Durum',
808
+ 'billing.invoice_status_paid': 'Ödendi',
809
+ 'billing.invoice_status_open': 'Ödeme bekleniyor',
810
+ 'billing.invoice_status_uncollectible': 'Ödenmedi',
811
+ 'billing.invoice_status_void': 'İptal edildi',
812
+ 'billing.invoice_status_draft': 'Taslak',
778
813
  'billing.invoices_empty': 'Henüz fatura yok',
779
814
  'billing.invoices_empty_hint': 'İlk ödemeden sonra burada bir fatura görünür.',
780
815
  'billing.invoices_section': 'Faturalar',
@@ -57,6 +57,35 @@ const locale = $derived(messageLocale())
57
57
  /** Where Stripe sends the person back to, and where the portal returns them. */
58
58
  const returnPath = $derived(`/${slug}/settings/billing/plan`)
59
59
 
60
+ /**
61
+ * Stripe's invoice statuses, said in words. An accountant reading the list needs to know which
62
+ * rows are still owed before they need the links; the list used to show number, date and amount
63
+ * and nothing about whether any of it had been paid.
64
+ */
65
+ const INVOICE_TONE: Record<string, 'success' | 'warning' | 'danger' | 'grey'> = {
66
+ paid: 'success',
67
+ open: 'warning',
68
+ uncollectible: 'danger',
69
+ void: 'grey',
70
+ draft: 'grey',
71
+ }
72
+ function invoiceStatus(status: string): string {
73
+ switch (status) {
74
+ case 'paid':
75
+ return t('invoice_status_paid')
76
+ case 'open':
77
+ return t('invoice_status_open')
78
+ case 'uncollectible':
79
+ return t('invoice_status_uncollectible')
80
+ case 'void':
81
+ return t('invoice_status_void')
82
+ case 'draft':
83
+ return t('invoice_status_draft')
84
+ default:
85
+ return status
86
+ }
87
+ }
88
+
60
89
  /**
61
90
  * What Stripe said on the way back, from `?checkout=`.
62
91
  *
@@ -501,10 +530,13 @@ function once(run: () => void) {
501
530
  />
502
531
  {:else}
503
532
  <div class="overflow-x-auto">
504
- <Table columns="minmax(120px,1fr) minmax(120px,1fr) minmax(100px,auto) minmax(80px,auto)">
533
+ <Table
534
+ columns="minmax(120px,1fr) minmax(120px,1fr) minmax(96px,auto) minmax(100px,auto) minmax(80px,auto)"
535
+ >
505
536
  <TableHeader>
506
537
  <TableCell header>{t('invoice_number')}</TableCell>
507
538
  <TableCell header>{t('invoice_date')}</TableCell>
539
+ <TableCell header>{t('invoice_status')}</TableCell>
508
540
  <TableCell header end>{t('invoice_amount')}</TableCell>
509
541
  <TableCell header end></TableCell>
510
542
  </TableHeader>
@@ -512,6 +544,9 @@ function once(run: () => void) {
512
544
  <TableRow>
513
545
  <TableCell>{inv.number ?? '—'}</TableCell>
514
546
  <TableCell>{day(inv.createdAt)}</TableCell>
547
+ <TableCell>
548
+ <Badge tone={INVOICE_TONE[inv.status] ?? 'grey'}>{invoiceStatus(inv.status)}</Badge>
549
+ </TableCell>
515
550
  <TableCell end>{formatMoney(inv.totalMinor, inv.currency, locale)}</TableCell>
516
551
  <TableCell end>
517
552
  <!-- Both links Stripe gives us: the hosted page (pay, see the receipt) and the PDF