Skip to content

Commit 92caade

Browse files
committed
Make billing events 1:1 with Stripe events and add unsynced and drift filters on accounts
1 parent 580248c commit 92caade

30 files changed

Lines changed: 910 additions & 657 deletions

application/account/BackOffice/routes/accounts/-components/AccountBillingEventRow.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ReactNode } from "react";
22

3-
import { Trans } from "@lingui/react/macro";
43
import { Badge } from "@repo/ui/components/Badge";
54
import { TableCell, TableRow } from "@repo/ui/components/Table";
65
import { formatCurrency } from "@repo/utils/currency/formatCurrency";
@@ -12,10 +11,6 @@ import { BILLING_EVENT_VARIANT } from "@/shared/lib/billingEventStyle";
1211

1312
type BillingEventSummary = components["schemas"]["BillingEventSummary"];
1413

15-
function isSameDay(a: string, b: string): boolean {
16-
return a.slice(0, 10) === b.slice(0, 10);
17-
}
18-
1914
export function AccountBillingEventRow({
2015
event,
2116
renderDate,
@@ -28,17 +23,11 @@ export function AccountBillingEventRow({
2823
const variant = BILLING_EVENT_VARIANT[event.eventType];
2924
const Icon = variant.icon;
3025
const showPlanTransition = event.fromPlan != null && event.toPlan != null && event.fromPlan !== event.toPlan;
31-
const showEffective = event.effectiveAt != null && !isSameDay(event.effectiveAt, event.occurredAt);
3226
return (
3327
<TableRow rowKey={event.id}>
3428
<TableCell className="align-top whitespace-nowrap">
3529
<div className="flex flex-col leading-tight">
3630
<span>{renderDate(event.occurredAt)}</span>
37-
{showEffective && (
38-
<span className="text-xs text-muted-foreground">
39-
<Trans>Effective {renderDate(event.effectiveAt)}</Trans>
40-
</span>
41-
)}
4231
</div>
4332
</TableCell>
4433
<TableCell>

application/account/BackOffice/routes/accounts/-components/AccountsToolbar.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { t } from "@lingui/core/macro";
22
import { Trans } from "@lingui/react/macro";
3+
import { Badge } from "@repo/ui/components/Badge";
4+
import { Button } from "@repo/ui/components/Button";
35
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@repo/ui/components/InputGroup";
46
import { ToggleGroup, ToggleGroupItem } from "@repo/ui/components/ToggleGroup";
57
import { useDebounce } from "@repo/ui/hooks/useDebounce";
68
import { useNavigate } from "@tanstack/react-router";
7-
import { SearchIcon, XIcon } from "lucide-react";
9+
import { CloudOffIcon, SearchIcon, TriangleAlertIcon, XIcon } from "lucide-react";
810
import { useEffect, useState } from "react";
911

1012
import type { SortableTenantProperties } from "@/shared/lib/api/client";
@@ -16,9 +18,11 @@ interface AccountsToolbarProps {
1618
search: string | undefined;
1719
plans: SubscriptionPlan[];
1820
statuses: TenantStatusFilter[];
21+
unsynced: boolean;
22+
driftDetected: boolean;
1923
}
2024

21-
export function AccountsToolbar({ search, plans, statuses }: Readonly<AccountsToolbarProps>) {
25+
export function AccountsToolbar({ search, plans, statuses, unsynced, driftDetected }: Readonly<AccountsToolbarProps>) {
2226
const navigate = useNavigate();
2327
const [searchInput, setSearchInput] = useState(search ?? "");
2428
const debouncedSearch = useDebounce(searchInput, 500);
@@ -137,6 +141,49 @@ export function AccountsToolbar({ search, plans, statuses }: Readonly<AccountsTo
137141
<Trans>Free</Trans>
138142
</ToggleGroupItem>
139143
</ToggleGroup>
144+
145+
<IssueFilterBadges unsynced={unsynced} driftDetected={driftDetected} />
140146
</div>
141147
);
142148
}
149+
150+
function IssueFilterBadges({ unsynced, driftDetected }: Readonly<{ unsynced: boolean; driftDetected: boolean }>) {
151+
const navigate = useNavigate();
152+
const clear = (key: "unsynced" | "driftDetected") => () =>
153+
navigate({
154+
to: "/accounts",
155+
search: (previous) => ({
156+
search: previous.search,
157+
plans: previous.plans,
158+
statuses: previous.statuses,
159+
unsynced: key === "unsynced" ? undefined : previous.unsynced,
160+
driftDetected: key === "driftDetected" ? undefined : previous.driftDetected,
161+
orderBy: previous.orderBy as SortableTenantProperties | undefined,
162+
sortOrder: previous.sortOrder,
163+
pageOffset: undefined
164+
})
165+
});
166+
167+
return (
168+
<>
169+
{unsynced && (
170+
<Badge variant="outline" className="gap-1.5 border-amber-500/30 text-amber-600">
171+
<CloudOffIcon className="size-3.5" aria-hidden={true} />
172+
<Trans>Not synced yet</Trans>
173+
<Button variant="ghost" size="icon-xs" aria-label={t`Clear filter`} onClick={clear("unsynced")}>
174+
<XIcon className="size-3" />
175+
</Button>
176+
</Badge>
177+
)}
178+
{driftDetected && (
179+
<Badge variant="outline" className="gap-1.5 border-amber-500/30 text-amber-600">
180+
<TriangleAlertIcon className="size-3.5" aria-hidden={true} />
181+
<Trans>Drift detected</Trans>
182+
<Button variant="ghost" size="icon-xs" aria-label={t`Clear filter`} onClick={clear("driftDetected")}>
183+
<XIcon className="size-3" />
184+
</Button>
185+
</Badge>
186+
)}
187+
</>
188+
);
189+
}

application/account/BackOffice/routes/accounts/index.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const accountsSearchSchema = z.object({
3131
search: z.string().optional(),
3232
plans: z.array(z.nativeEnum(SubscriptionPlan)).max(10).optional(),
3333
statuses: z.array(z.nativeEnum(TenantStatusFilter)).max(10).optional(),
34+
unsynced: z.boolean().optional(),
35+
driftDetected: z.boolean().optional(),
3436
orderBy: z.nativeEnum(SortableTenantProperties).optional(),
3537
sortOrder: z.nativeEnum(SortOrder).optional(),
3638
pageOffset: z.number().int().nonnegative().optional()
@@ -43,7 +45,7 @@ export const Route = createFileRoute("/accounts/")({
4345
});
4446

4547
function AccountsListPage() {
46-
const { search, plans, statuses, orderBy, sortOrder, pageOffset } = Route.useSearch();
48+
const { search, plans, statuses, unsynced, driftDetected, orderBy, sortOrder, pageOffset } = Route.useSearch();
4749
const navigate = useNavigate();
4850
const [previewTenant, setPreviewTenant] = useState<TenantSummary | null>(null);
4951

@@ -56,6 +58,8 @@ function AccountsListPage() {
5658
Search: search,
5759
Plans: plans,
5860
Statuses: statuses,
61+
Unsynced: unsynced,
62+
DriftDetected: driftDetected,
5963
OrderBy: orderBy,
6064
SortOrder: sortOrder,
6165
PageOffset: pageOffset
@@ -72,7 +76,12 @@ function AccountsListPage() {
7276
const handleClosePane = useCallback(() => setPreviewTenant(null), []);
7377

7478
const tenants = data?.tenants ?? [];
75-
const hasFilters = Boolean(search) || (plans?.length ?? 0) > 0 || (statuses?.length ?? 0) > 0;
79+
const hasFilters =
80+
Boolean(search) ||
81+
(plans?.length ?? 0) > 0 ||
82+
(statuses?.length ?? 0) > 0 ||
83+
Boolean(unsynced) ||
84+
Boolean(driftDetected);
7685
const showEmpty = !isLoading && tenants.length === 0;
7786

7887
return (
@@ -91,7 +100,13 @@ function AccountsListPage() {
91100
) : undefined
92101
}
93102
>
94-
<AccountsToolbar search={search} plans={plans ?? []} statuses={statuses ?? []} />
103+
<AccountsToolbar
104+
search={search}
105+
plans={plans ?? []}
106+
statuses={statuses ?? []}
107+
unsynced={unsynced ?? false}
108+
driftDetected={driftDetected ?? false}
109+
/>
95110

96111
{showEmpty ? (
97112
<Empty>

application/account/BackOffice/shared/components/BillingDriftBanner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function BillingDriftBanner() {
3131
<span className="flex-1 text-warning-foreground">
3232
<Trans>{count} accounts have billing drift detected.</Trans>
3333
</span>
34-
<Button size="sm" nativeButton={false} render={<Link to="/accounts" />}>
34+
<Button size="sm" nativeButton={false} render={<Link to="/accounts" search={{ driftDetected: true }} />}>
3535
<Trans>View accounts</Trans>
3636
</Button>
3737
</div>

application/account/BackOffice/shared/components/UnsyncedAccountsBanner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function UnsyncedAccountsBanner() {
3232
<span className="flex-1 text-warning-foreground">
3333
<Trans>{count} accounts have not been synced yet — MRR trend is incomplete.</Trans>
3434
</span>
35-
<Button size="sm" nativeButton={false} render={<Link to="/accounts" />}>
35+
<Button size="sm" nativeButton={false} render={<Link to="/accounts" search={{ unsynced: true }} />}>
3636
<Trans>View accounts</Trans>
3737
</Button>
3838
</div>

application/account/BackOffice/shared/lib/api/labels.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ export function getBillingEventTypeLabel(type: BillingEventType): string {
136136
return t`Billing info updated`;
137137
case BillingEventType.PaymentMethodUpdated:
138138
return t`Payment method updated`;
139+
case BillingEventType.NoOp:
140+
return t`No change`;
141+
case BillingEventType.Unclassified:
142+
return t`Unclassified`;
139143
default:
140144
return String(type);
141145
}

application/account/BackOffice/shared/lib/billingEventStyle.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import {
44
CalendarClockIcon,
55
CircleAlertIcon,
66
CircleCheckIcon,
7+
CircleSlashIcon,
78
CircleXIcon,
89
CreditCardIcon,
910
PauseCircleIcon,
1011
RefreshCwIcon,
1112
ReplyIcon,
1213
RotateCcwIcon,
14+
TriangleAlertIcon,
1315
WalletIcon
1416
} from "lucide-react";
1517

@@ -93,5 +95,13 @@ export const BILLING_EVENT_VARIANT: Record<BillingEventType, BillingEventVariant
9395
[BillingEventType.PaymentMethodUpdated]: {
9496
className: "bg-sky-500/10 text-sky-500 border-sky-500/20",
9597
icon: CreditCardIcon
98+
},
99+
[BillingEventType.NoOp]: {
100+
className: "bg-muted text-muted-foreground border-border",
101+
icon: CircleSlashIcon
102+
},
103+
[BillingEventType.Unclassified]: {
104+
className: "bg-amber-500/10 text-amber-600 border-amber-500/30",
105+
icon: TriangleAlertIcon
96106
}
97107
};

application/account/BackOffice/shared/translations/locale/da-DK.po

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ msgstr "Skift tema"
209209
msgid "Change zoom level"
210210
msgstr "Skift zoomniveau"
211211

212+
msgid "Clear filter"
213+
msgstr ""
214+
212215
msgid "Clear filters"
213216
msgstr "Ryd filtre"
214217

@@ -282,9 +285,8 @@ msgstr "Nedgraderet"
282285
msgid "Downgrading"
283286
msgstr "Nedgraderer"
284287

285-
#. placeholder {0}: renderDate(event.effectiveAt)
286-
msgid "Effective {0}"
287-
msgstr "Gælder fra {0}"
288+
msgid "Drift detected"
289+
msgstr ""
288290

289291
msgid "Email"
290292
msgstr "E-mail"
@@ -472,6 +474,9 @@ msgstr "Ingen faktureringshændelser matcher dine filtre"
472474
msgid "No billing events yet"
473475
msgstr "Ingen faktureringshændelser endnu"
474476

477+
msgid "No change"
478+
msgstr ""
479+
475480
msgid "No invoices, refunds, or credit notes yet."
476481
msgstr "Ingen fakturaer, refusioner eller kreditnotaer endnu."
477482

@@ -526,6 +531,9 @@ msgstr "Ingen brugere matcher din søgning"
526531
msgid "No users yet"
527532
msgstr "Ingen brugere endnu"
528533

534+
msgid "Not synced yet"
535+
msgstr ""
536+
529537
msgid "Occurred"
530538
msgstr "Tidspunkt"
531539

@@ -786,6 +794,9 @@ msgstr "Prøv igen"
786794
msgid "Try clearing the search or filters to see more results."
787795
msgstr "Prøv at rydde søgningen eller filtrene for at se flere resultater."
788796

797+
msgid "Unclassified"
798+
msgstr ""
799+
789800
msgid "Unknown"
790801
msgstr "Ukendt"
791802

application/account/BackOffice/shared/translations/locale/en-US.po

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,9 @@ msgstr "Change theme"
209209
msgid "Change zoom level"
210210
msgstr "Change zoom level"
211211

212+
msgid "Clear filter"
213+
msgstr "Clear filter"
214+
212215
msgid "Clear filters"
213216
msgstr "Clear filters"
214217

@@ -282,9 +285,8 @@ msgstr "Downgraded"
282285
msgid "Downgrading"
283286
msgstr "Downgrading"
284287

285-
#. placeholder {0}: renderDate(event.effectiveAt)
286-
msgid "Effective {0}"
287-
msgstr "Effective {0}"
288+
msgid "Drift detected"
289+
msgstr "Drift detected"
288290

289291
msgid "Email"
290292
msgstr "Email"
@@ -472,6 +474,9 @@ msgstr "No billing events match your filters"
472474
msgid "No billing events yet"
473475
msgstr "No billing events yet"
474476

477+
msgid "No change"
478+
msgstr "No change"
479+
475480
msgid "No invoices, refunds, or credit notes yet."
476481
msgstr "No invoices, refunds, or credit notes yet."
477482

@@ -526,6 +531,9 @@ msgstr "No users match your search"
526531
msgid "No users yet"
527532
msgstr "No users yet"
528533

534+
msgid "Not synced yet"
535+
msgstr "Not synced yet"
536+
529537
msgid "Occurred"
530538
msgstr "Occurred"
531539

@@ -786,6 +794,9 @@ msgstr "Try again"
786794
msgid "Try clearing the search or filters to see more results."
787795
msgstr "Try clearing the search or filters to see more results."
788796

797+
msgid "Unclassified"
798+
msgstr "Unclassified"
799+
789800
msgid "Unknown"
790801
msgstr "Unknown"
791802

0 commit comments

Comments
 (0)