Skip to content

Commit 300c96c

Browse files
authored
Merge pull request #1140 from useautumn/frontend-customer-cleanup
Frontend customer cleanup
2 parents e7f8cc5 + e877a1e commit 300c96c

File tree

6 files changed

+149
-53
lines changed

6 files changed

+149
-53
lines changed

server/src/internal/balances/updateBalance/runRedisUpdateBalanceV2.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,19 @@ export const runRedisUpdateBalanceV2 = async ({
7070
throw error;
7171
}
7272

73-
const { updates } = result;
73+
const { updates, rolloverUpdates } = result;
7474

75-
// Sync to Postgres (update balance doesn't touch rollovers)
7675
const modifiedCusEntIds = deductionUpdatesToModifiedIds({ updates });
76+
const modifiedRolloverIds = Object.keys(rolloverUpdates);
7777

78-
if (modifiedCusEntIds.length > 0) {
78+
if (modifiedCusEntIds.length > 0 || modifiedRolloverIds.length > 0) {
7979
await syncItemV3({
8080
payload: {
8181
customerId,
8282
orgId: org.id,
8383
env,
8484
cusEntIds: modifiedCusEntIds,
85+
rolloverIds: modifiedRolloverIds,
8586
region: currentRegion,
8687
timestamp: Date.now(),
8788
},

vite/src/views/customers2/components/CustomerBillingControlsSection.tsx

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
AutoTopup,
3+
DbOverageAllowed,
34
DbSpendLimit,
45
DbUsageAlert,
56
Entity,
@@ -150,7 +151,9 @@ const UsageAlertRow = ({
150151
})}
151152
</span>
152153
{usageAlert.name && (
153-
<span className="truncate text-xs text-t3 font-mono ml-4">{usageAlert.name}</span>
154+
<span className="truncate text-xs text-t3 font-mono ml-4">
155+
{usageAlert.name}
156+
</span>
154157
)}
155158
<div className="ml-auto flex items-center gap-1.5 shrink-0">
156159
<Pill>At: {thresholdLabel}</Pill>
@@ -164,6 +167,24 @@ const UsageAlertRow = ({
164167
);
165168
};
166169

170+
const OverageAllowedRow = ({
171+
overageAllowed,
172+
featureNameById,
173+
}: {
174+
overageAllowed: DbOverageAllowed;
175+
featureNameById: Map<string, string>;
176+
}) => (
177+
<div className={rowClassName}>
178+
<StatusPill enabled={overageAllowed.enabled} />
179+
<span className="truncate text-sm text-t1 font-medium">
180+
{getFeatureLabel({
181+
featureId: overageAllowed.feature_id,
182+
featureNameById,
183+
})}
184+
</span>
185+
</div>
186+
);
187+
167188
export function CustomerBillingControlsSection() {
168189
const { customer, features, isLoading } = useCusQuery();
169190
const { entityId } = useCustomerContext();
@@ -194,15 +215,22 @@ export function CustomerBillingControlsSection() {
194215
const usageAlerts = selectedEntity
195216
? (selectedEntity.usage_alerts ?? [])
196217
: (fullCustomer?.usage_alerts ?? []);
218+
const overageAllowed = selectedEntity
219+
? (selectedEntity.overage_allowed ?? [])
220+
: (fullCustomer?.overage_allowed ?? []);
197221

198222
const hasAnyControls =
199-
autoTopups.length > 0 || spendLimits.length > 0 || usageAlerts.length > 0;
223+
autoTopups.length > 0 ||
224+
spendLimits.length > 0 ||
225+
usageAlerts.length > 0 ||
226+
overageAllowed.length > 0;
200227

201228
const entitiesWithControlsCount =
202229
fullCustomer?.entities?.filter(
203230
(entity: Entity) =>
204231
(entity.spend_limits?.length ?? 0) > 0 ||
205-
(entity.usage_alerts?.length ?? 0) > 0,
232+
(entity.usage_alerts?.length ?? 0) > 0 ||
233+
(entity.overage_allowed?.length ?? 0) > 0,
206234
).length ?? 0;
207235

208236
if (!isLoading && !hasAnyControls && selectedEntity) return null;
@@ -216,11 +244,7 @@ export function CustomerBillingControlsSection() {
216244
<Table.Container>
217245
<Table.Toolbar>
218246
<Table.Heading>
219-
<GavelIcon
220-
size={16}
221-
weight="fill"
222-
className="text-subtle"
223-
/>
247+
<GavelIcon size={16} weight="fill" className="text-subtle" />
224248
Billing controls
225249
</Table.Heading>
226250
</Table.Toolbar>
@@ -232,11 +256,7 @@ export function CustomerBillingControlsSection() {
232256
) : (
233257
<div className="flex flex-col gap-4">
234258
{autoTopups.length > 0 && (
235-
<BillingControlsGroup
236-
title="Auto top-ups"
237-
emptyText=""
238-
hasItems
239-
>
259+
<BillingControlsGroup title="Auto top-ups" emptyText="" hasItems>
240260
<div className="flex flex-col gap-1.5">
241261
{autoTopups.map((autoTopup) => (
242262
<AutoTopupRow
@@ -250,11 +270,7 @@ export function CustomerBillingControlsSection() {
250270
)}
251271

252272
{spendLimits.length > 0 && (
253-
<BillingControlsGroup
254-
title="Spend limits"
255-
emptyText=""
256-
hasItems
257-
>
273+
<BillingControlsGroup title="Spend limits" emptyText="" hasItems>
258274
<div className="flex flex-col gap-1.5">
259275
{spendLimits.map((spendLimit, index) => (
260276
<SpendLimitRow
@@ -268,11 +284,7 @@ export function CustomerBillingControlsSection() {
268284
)}
269285

270286
{usageAlerts.length > 0 && (
271-
<BillingControlsGroup
272-
title="Usage alerts"
273-
emptyText=""
274-
hasItems
275-
>
287+
<BillingControlsGroup title="Usage alerts" emptyText="" hasItems>
276288
<div className="flex flex-col gap-1.5">
277289
{usageAlerts.map((usageAlert, index) => (
278290
<UsageAlertRow
@@ -284,6 +296,20 @@ export function CustomerBillingControlsSection() {
284296
</div>
285297
</BillingControlsGroup>
286298
)}
299+
300+
{overageAllowed.length > 0 && (
301+
<BillingControlsGroup title="Overage enabled" emptyText="" hasItems>
302+
<div className="flex flex-col gap-1.5">
303+
{overageAllowed.map((item) => (
304+
<OverageAllowedRow
305+
key={`overage-allowed-${item.feature_id}`}
306+
overageAllowed={item}
307+
featureNameById={featureNameById}
308+
/>
309+
))}
310+
</div>
311+
</BillingControlsGroup>
312+
)}
287313
</div>
288314
)}
289315
</Table.Container>

vite/src/views/customers2/components/sheets/BalanceEditSheet.tsx

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isUnlimitedCusEnt,
99
numberWithCommas,
1010
} from "@autumn/shared";
11+
import { ClockCountdownIcon } from "@phosphor-icons/react";
1112

1213
import { useStore } from "@tanstack/react-form";
1314
import { useState } from "react";
@@ -129,6 +130,7 @@ function UnlimitedBalanceInfo({
129130
isUnlimited
130131
/>
131132
</SheetSection>
133+
<RolloversSection selectedCusEnt={selectedCusEnt} />
132134
</div>
133135
);
134136
}
@@ -166,6 +168,8 @@ function BalanceEditForm({
166168
/>
167169
</SheetSection>
168170

171+
<RolloversSection selectedCusEnt={selectedCusEnt} entityId={entityId} />
172+
169173
<SheetSection withSeparator={false}>
170174
<BalanceFields
171175
form={form}
@@ -186,6 +190,61 @@ function BalanceEditForm({
186190
);
187191
}
188192

193+
/* ─── Rollovers Section ─── */
194+
195+
function RolloversSection({
196+
selectedCusEnt,
197+
entityId,
198+
}: {
199+
selectedCusEnt: FullCustomerEntitlement;
200+
entityId?: string | null;
201+
}) {
202+
const rolloverFields = getRolloverFields({
203+
cusEnt: selectedCusEnt,
204+
entityId: entityId ?? undefined,
205+
});
206+
207+
if (!rolloverFields || rolloverFields.rollovers.length === 0) return null;
208+
209+
const { rollovers } = rolloverFields;
210+
211+
return (
212+
<SheetSection withSeparator>
213+
<div className="flex flex-col gap-2">
214+
<div className="flex items-center gap-1.5 text-t3 text-sm font-medium">
215+
<ClockCountdownIcon size={14} weight="duotone" />
216+
Rollovers
217+
</div>
218+
<div className="flex flex-col gap-1.5">
219+
{rollovers.map((rollover, index) => {
220+
const expiryText = rollover.expires_at
221+
? `${formatUnixToDateTime(rollover.expires_at, { withYear: true }).date}, ${formatUnixToDateTime(rollover.expires_at).time}`
222+
: "No expiry";
223+
224+
return (
225+
<div
226+
key={`rollover-${index}-${rollover.expires_at}`}
227+
className="flex items-center justify-between text-sm px-2 py-0.5 rounded-md"
228+
>
229+
<span className="text-t1 font-medium">
230+
+{numberWithCommas(rollover.balance)}
231+
</span>
232+
<span className="text-t3 text-xs">
233+
{rollover.expires_at ? `Expires ${expiryText}` : expiryText}
234+
</span>
235+
</div>
236+
);
237+
})}
238+
</div>
239+
<div className="flex items-center justify-between text-xs text-t4 px-2">
240+
<span>Total rollover</span>
241+
<span>+{numberWithCommas(rolloverFields.balance)}</span>
242+
</div>
243+
</div>
244+
</SheetSection>
245+
);
246+
}
247+
189248
/* ─── Entitlement Info Rows ─── */
190249

191250
function EntitlementInfoRows({
@@ -328,9 +387,6 @@ function SetBalanceFields({
328387
const balance = useStore(form.store, (s) => s.values.balance);
329388
const gpb = useStore(form.store, (s) => s.values.grantedAndPurchasedBalance);
330389

331-
const rolloverBalance =
332-
getRolloverFields({ cusEnt: selectedCusEnt })?.balance ?? 0;
333-
334390
return (
335391
<div className="flex flex-col gap-3">
336392
<div className="flex items-end gap-2 w-full">
@@ -368,13 +424,11 @@ function SetBalanceFields({
368424
)}
369425
<div className="text-t4 text-sm truncate mb-1 flex justify-center max-w-full w-full">
370426
<span className="truncate">
371-
{numberWithCommas((gpb ?? 0) - (balance ?? 0))} used
427+
{numberWithCommas(
428+
(gpb ?? 0) + form.rolloverBalance - (balance ?? 0),
429+
)}{" "}
430+
used
372431
</span>
373-
{/* {rolloverBalance > 0 && (
374-
<span className="truncate">
375-
+{numberWithCommas(rolloverBalance)} rollover
376-
</span>
377-
)} */}
378432
</div>
379433
</div>
380434
</div>
@@ -503,11 +557,14 @@ function SubmitButton({
503557
prepaidAllowance: form.prepaidAllowance,
504558
});
505559

560+
const targetBalance =
561+
parseFloat(String(values.balance)) - form.rolloverBalance;
562+
506563
promises.push(
507564
axiosInstance.post("/v1/balances/update", {
508565
customer_id: customer.id || customer.internal_id,
509566
feature_id: featureId,
510-
current_balance: parseFloat(String(values.balance)),
567+
current_balance: targetBalance,
511568
included_grant: grantedBalanceInput ?? undefined,
512569
granted_balance: grantedBalanceInput ?? undefined,
513570
customer_entitlement_id: selectedCusEnt.id,

vite/src/views/customers2/components/sheets/SubscriptionDetailSheet.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
type Entity,
44
isCustomerProductTrialing,
55
type ProductItem,
6+
UsageModel,
67
} from "@autumn/shared";
78
import {
89
ArrowSquareOutIcon,
@@ -152,22 +153,24 @@ export function SubscriptionDetailSheet() {
152153
</div>
153154
)}
154155

155-
<div className="space-y-2">
156-
{productV2.items.map((item: ProductItem, index: number) => {
157-
if (!item.feature_id) return null;
158-
const prepaidQuantity =
159-
prepaidDisplayQuantities[item.feature_id] ?? null;
156+
<div className="space-y-2">
157+
{productV2.items.map((item: ProductItem, index: number) => {
158+
if (!item.feature_id) return null;
159+
const prepaidQuantity =
160+
item.usage_model === UsageModel.Prepaid
161+
? (prepaidDisplayQuantities[item.feature_id] ?? null)
162+
: null;
160163

161-
return (
162-
<PlanFeatureRow
163-
key={item.feature_id || item.price_id || index}
164-
item={item}
165-
index={index}
166-
readOnly={true}
167-
prepaidQuantity={prepaidQuantity}
168-
/>
169-
);
170-
})}
164+
return (
165+
<PlanFeatureRow
166+
key={item.feature_id || item.price_id || index}
167+
item={item}
168+
index={index}
169+
readOnly={true}
170+
prepaidQuantity={prepaidQuantity}
171+
/>
172+
);
173+
})}
171174
</div>
172175
</SheetSection>
173176
)}

vite/src/views/customers2/components/sheets/useBalanceEditForm.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
cusEntsToGrantedBalance,
44
cusEntsToPrepaidQuantity,
55
type FullCusEntWithFullCusProduct,
6+
getRolloverFields,
67
nullish,
78
} from "@autumn/shared";
89
import { useAppForm } from "@/hooks/form/form";
@@ -26,7 +27,7 @@ export function useBalanceEditForm({
2627
const balance = cusEntsToBalance({
2728
cusEnts: [selectedCusEnt],
2829
entityId: entityId ?? undefined,
29-
withRollovers: false,
30+
withRollovers: true,
3031
});
3132

3233
const grantedBalance = cusEntsToGrantedBalance({
@@ -36,6 +37,12 @@ export function useBalanceEditForm({
3637

3738
const grantedAndPurchasedBalance = grantedBalance + prepaidAllowance;
3839

40+
const rolloverBalance =
41+
getRolloverFields({
42+
cusEnt: selectedCusEnt,
43+
entityId: entityId ?? undefined,
44+
})?.balance ?? 0;
45+
3946
const form = useAppForm({
4047
defaultValues: {
4148
mode: "set",
@@ -49,7 +56,7 @@ export function useBalanceEditForm({
4956
},
5057
});
5158

52-
return Object.assign(form, { prepaidAllowance });
59+
return Object.assign(form, { prepaidAllowance, rolloverBalance });
5360
}
5461

5562
export type BalanceEditFormInstance = ReturnType<typeof useBalanceEditForm>;

vite/src/views/customers2/components/table/customer-balance/CustomerBalanceTableColumns.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ function SubRowUsageCell({
189189
return <span className="text-t4">Unlimited</span>;
190190
}
191191

192+
193+
192194
const { balance, allowance, rolloverBalance } = getIndividualEntValues({
193195
ent,
194196
entityId,

0 commit comments

Comments
 (0)