Merge pull request #1266 from useautumn/fix/max-entitis
fix: max entitis
This commit is contained in:
@@ -114,7 +114,7 @@ const buildEntitiesCTE = (withEntities: boolean) => {
|
||||
SELECT * FROM entities e
|
||||
WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record)
|
||||
ORDER BY e.internal_id DESC
|
||||
LIMIT 1000
|
||||
LIMIT 300
|
||||
) e
|
||||
)
|
||||
`;
|
||||
@@ -692,7 +692,7 @@ export const getPaginatedFullCusQuery = ({
|
||||
FROM entities e
|
||||
WHERE e.internal_customer_id = cr.internal_id
|
||||
ORDER BY e.internal_id DESC
|
||||
LIMIT 1000
|
||||
LIMIT 300
|
||||
) e ON true
|
||||
GROUP BY cr.internal_id
|
||||
)`
|
||||
|
||||
@@ -216,11 +216,6 @@ export class InvoiceService {
|
||||
currency: stripeInvoice.currency,
|
||||
});
|
||||
|
||||
const atmnAmountPaid = stripeToAtmnAmount({
|
||||
amount: stripeInvoice.amount_paid,
|
||||
currency: stripeInvoice.currency,
|
||||
});
|
||||
|
||||
const invoice: Invoice = {
|
||||
id: generateId("inv"),
|
||||
internal_customer_id: internalCustomerId,
|
||||
@@ -234,7 +229,6 @@ export class InvoiceService {
|
||||
|
||||
// Stripe stuff
|
||||
total: atmnTotal,
|
||||
amount_paid: atmnAmountPaid,
|
||||
refunded_amount: 0,
|
||||
currency: stripeInvoice.currency,
|
||||
discounts: getInvoiceDiscounts({
|
||||
@@ -329,7 +323,6 @@ export class InvoiceService {
|
||||
hosted_invoice_url: invoice.hosted_invoice_url,
|
||||
discounts: invoice.discounts,
|
||||
total: invoice.total,
|
||||
amount_paid: invoice.amount_paid,
|
||||
product_ids: invoice.product_ids?.length
|
||||
? sql`CASE
|
||||
WHEN ${invoices.product_ids} IS NULL OR cardinality(${invoices.product_ids}) = 0
|
||||
|
||||
@@ -29,10 +29,6 @@ export const updateInvoiceFromStripe = async ({
|
||||
amount: stripeInvoice.total,
|
||||
currency: stripeInvoice.currency,
|
||||
}),
|
||||
amount_paid: stripeToAtmnAmount({
|
||||
amount: stripeInvoice.amount_paid,
|
||||
currency: stripeInvoice.currency,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -50,11 +50,6 @@ export const initInvoiceFromStripe = async ({
|
||||
currency: stripeInvoice.currency,
|
||||
});
|
||||
|
||||
const atmnAmountPaid = stripeToAtmnAmount({
|
||||
amount: stripeInvoice.amount_paid,
|
||||
currency: stripeInvoice.currency,
|
||||
});
|
||||
|
||||
return {
|
||||
id: generateId("inv"),
|
||||
internal_customer_id: internalCustomerId,
|
||||
@@ -66,7 +61,6 @@ export const initInvoiceFromStripe = async ({
|
||||
status: stripeInvoice.status as string | undefined,
|
||||
internal_entity_id: internalEntityId || null,
|
||||
total: atmnTotal,
|
||||
amount_paid: atmnAmountPaid,
|
||||
currency: stripeInvoice.currency,
|
||||
discounts: getInvoiceDiscounts({ expandedInvoice: stripeInvoice }),
|
||||
items: autumnInvoiceItems,
|
||||
|
||||
@@ -39,7 +39,6 @@ export const InvoiceSchema = z.object({
|
||||
|
||||
// Total amount of the invoice
|
||||
total: z.number(),
|
||||
amount_paid: z.number().nullish(),
|
||||
refunded_amount: z.number().default(0),
|
||||
currency: z.string(),
|
||||
receipt_url: z.string().nullish(),
|
||||
|
||||
@@ -28,7 +28,6 @@ export const invoices = pgTable(
|
||||
status: text("status").notNull().default("draft"),
|
||||
hosted_invoice_url: text("hosted_invoice_url"),
|
||||
total: numeric({ mode: "number" }).notNull().default(0),
|
||||
amount_paid: numeric({ mode: "number" }),
|
||||
refunded_amount: numeric({ mode: "number" }).notNull().default(0),
|
||||
currency: text("currency").notNull().default("usd"),
|
||||
discounts: jsonb("discounts").$type<InvoiceDiscount>().array().default([]),
|
||||
|
||||
@@ -8,9 +8,8 @@ import {
|
||||
import {
|
||||
ArrowCounterClockwiseIcon,
|
||||
ArrowSquareOutIcon,
|
||||
CalendarBlankIcon,
|
||||
CreditCardIcon,
|
||||
HashIcon,
|
||||
EyeIcon,
|
||||
EyeSlashIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -18,12 +17,10 @@ import { AdminHover } from "@/components/general/AdminHover";
|
||||
import { Badge } from "@/components/v2/badges/Badge";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import { MiniCopyButton } from "@/components/v2/buttons/CopyButton";
|
||||
import { InfoRow } from "@/components/v2/InfoRow";
|
||||
import { SheetHeader, SheetSection } from "@/components/v2/sheets/InlineSheet";
|
||||
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
|
||||
import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery";
|
||||
import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
|
||||
import { useSheetStore } from "@/hooks/stores/useSheetStore";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEnv } from "@/utils/envUtils";
|
||||
import { getStripeInvoiceLink } from "@/utils/linkUtils";
|
||||
@@ -31,6 +28,11 @@ import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
import { CustomerInvoiceStatus } from "../table/customer-invoices/CustomerInvoiceStatus";
|
||||
import { RefundInvoiceDialog } from "./RefundInvoiceDialog";
|
||||
|
||||
interface InvoiceDetailSheetProps {
|
||||
invoice: Invoice;
|
||||
lineItems: InvoiceLineItem[];
|
||||
}
|
||||
|
||||
type LineItemGroup = {
|
||||
groupKey: string;
|
||||
label: string;
|
||||
@@ -58,19 +60,29 @@ const resolveFeatureName = ({
|
||||
return feature?.name ?? featureId;
|
||||
};
|
||||
|
||||
export function InvoiceDetailSheet() {
|
||||
const sheetData = useSheetStore((s) => s.data);
|
||||
const invoice = sheetData?.invoice as Invoice | undefined;
|
||||
const lineItems = (sheetData?.lineItems as InvoiceLineItem[]) ?? [];
|
||||
|
||||
export function InvoiceDetailSheet({
|
||||
invoice,
|
||||
lineItems,
|
||||
}: InvoiceDetailSheetProps) {
|
||||
const { stripeAccount } = useOrgStripeQuery();
|
||||
const { features } = useFeaturesQuery();
|
||||
const { products } = useProductsQuery();
|
||||
const env = useEnv();
|
||||
const [showDescriptions, setShowDescriptions] = useState(false);
|
||||
const [refundDialogOpen, setRefundDialogOpen] = useState(false);
|
||||
const { customer } = useCusQuery();
|
||||
const isStripeCustomer = customer?.processor?.type === ProcessorType.Stripe;
|
||||
const isFullyRefunded =
|
||||
invoice.refunded_amount > 0 &&
|
||||
invoice.refunded_amount >= Math.abs(invoice.total);
|
||||
const canRefund =
|
||||
isStripeCustomer &&
|
||||
invoice.status === InvoiceStatus.Paid &&
|
||||
!isFullyRefunded;
|
||||
|
||||
|
||||
// Group line items: first by product_id, then within each product by
|
||||
// stripe_subscription_item_id (for tiered) or individually.
|
||||
// Sort: base price first, then alphabetically by label within each product.
|
||||
const productGroups = useMemo(() => {
|
||||
// Step 1: bucket line items by product_id
|
||||
const byProduct = new Map<string, InvoiceLineItem[]>();
|
||||
@@ -134,17 +146,6 @@ export function InvoiceDetailSheet() {
|
||||
return result;
|
||||
}, [lineItems, features, products]);
|
||||
|
||||
if (!invoice) return null;
|
||||
|
||||
const isStripeCustomer = customer?.processor?.type === ProcessorType.Stripe;
|
||||
const refundableAmount = Math.abs(invoice.amount_paid ?? invoice.total);
|
||||
const isFullyRefunded =
|
||||
invoice.refunded_amount > 0 && invoice.refunded_amount >= refundableAmount;
|
||||
const canRefund =
|
||||
isStripeCustomer &&
|
||||
invoice.status === InvoiceStatus.Paid &&
|
||||
!isFullyRefunded;
|
||||
|
||||
const formatAmount = (amount: number, currency: string) => {
|
||||
const absAmount = Math.abs(amount);
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
@@ -188,14 +189,13 @@ export function InvoiceDetailSheet() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-y-auto">
|
||||
<div className="flex flex-col h-full">
|
||||
<SheetHeader
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Invoice</span>
|
||||
<CustomerInvoiceStatus
|
||||
status={invoice.status ?? "paid"}
|
||||
amountPaid={invoice.amount_paid}
|
||||
total={invoice.total}
|
||||
refundedAmount={invoice.refunded_amount}
|
||||
/>
|
||||
@@ -204,86 +204,91 @@ export function InvoiceDetailSheet() {
|
||||
description={`${formatDate(invoice.created_at)} • ${formatSignedAmount(invoice.total, invoice.currency)}`}
|
||||
/>
|
||||
|
||||
{productGroups.map((productGroup) => (
|
||||
<SheetSection
|
||||
key={productGroup.productId ?? "unknown"}
|
||||
withSeparator={true}
|
||||
>
|
||||
<div className="mb-2">
|
||||
<span className="text-xs font-medium text-t3 truncate">
|
||||
{productGroup.productName}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
{/* Product groups with line items */}
|
||||
{productGroups.map((productGroup) => (
|
||||
<SheetSection
|
||||
key={productGroup.productId ?? "unknown"}
|
||||
withSeparator={true}
|
||||
>
|
||||
{/* Product header with description toggle */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium text-t3 truncate">
|
||||
{productGroup.productName}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowDescriptions((prev) => !prev)}
|
||||
className="text-t4 hover:text-t2 transition-colors p-0.5 rounded"
|
||||
title={
|
||||
showDescriptions
|
||||
? "Show computed display"
|
||||
: "Show descriptions"
|
||||
}
|
||||
>
|
||||
{showDescriptions ? (
|
||||
<EyeSlashIcon size={14} />
|
||||
) : (
|
||||
<EyeIcon size={14} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{productGroup.lineItemGroups.map((group) => (
|
||||
<LineItemGroupRow
|
||||
key={group.groupKey}
|
||||
group={group}
|
||||
formatAmount={formatAmount}
|
||||
formatPeriod={formatPeriod}
|
||||
currency={invoice.currency}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SheetSection>
|
||||
))}
|
||||
{/* Line item groups within this product */}
|
||||
<div className="flex flex-col gap-2">
|
||||
{productGroup.lineItemGroups.map((group) => (
|
||||
<LineItemGroupRow
|
||||
key={group.groupKey}
|
||||
group={group}
|
||||
formatAmount={formatAmount}
|
||||
formatPeriod={formatPeriod}
|
||||
currency={invoice.currency}
|
||||
showDescriptions={showDescriptions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SheetSection>
|
||||
))}
|
||||
|
||||
<SheetSection withSeparator={true}>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{/* Invoice Total */}
|
||||
<SheetSection withSeparator={true}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-foreground">Total</span>
|
||||
<span className="text-sm font-semibold text-foreground tabular-nums">
|
||||
{formatSignedAmount(invoice.total, invoice.currency)}
|
||||
</span>
|
||||
</div>
|
||||
{invoice.amount_paid != null && invoice.amount_paid !== invoice.total && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-t2">Amount Paid</span>
|
||||
<span className="text-sm tabular-nums text-t2">
|
||||
{formatSignedAmount(invoice.amount_paid, invoice.currency)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SheetSection>
|
||||
</SheetSection>
|
||||
|
||||
<SheetSection withSeparator={false}>
|
||||
<div className="space-y-3">
|
||||
<InfoRow
|
||||
icon={<HashIcon size={16} weight="duotone" />}
|
||||
label="Invoice ID"
|
||||
value={
|
||||
{/* Invoice Details - Compact */}
|
||||
<SheetSection withSeparator={false}>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-t3">Invoice ID</span>
|
||||
<MiniCopyButton
|
||||
text={invoice.id}
|
||||
innerClassName="text-sm text-t1 font-mono"
|
||||
innerClassName="text-xs text-t1 font-mono"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={<CreditCardIcon size={16} weight="duotone" />}
|
||||
label="Stripe ID"
|
||||
value={
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-t3">Stripe ID</span>
|
||||
<MiniCopyButton
|
||||
text={invoice.stripe_id}
|
||||
innerClassName="text-sm text-t1 font-mono"
|
||||
innerClassName="text-xs text-t1 font-mono"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InfoRow
|
||||
icon={<CalendarBlankIcon size={16} weight="duotone" />}
|
||||
label="Created"
|
||||
value={
|
||||
<MiniCopyButton
|
||||
text={format(new Date(invoice.created_at), "MMM d, yyyy HH:mm")}
|
||||
innerClassName="text-sm text-t1"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SheetSection>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-t3">Created</span>
|
||||
<span className="text-t1">
|
||||
{format(new Date(invoice.created_at), "MMM d, yyyy HH:mm")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</SheetSection>
|
||||
</div>
|
||||
|
||||
<div className="sticky bottom-0 p-4 flex gap-2 bg-card">
|
||||
{/* Footer */}
|
||||
<div className="p-4 flex gap-2 border-t border-border/40">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="flex-1"
|
||||
@@ -319,11 +324,13 @@ function LineItemGroupRow({
|
||||
formatAmount,
|
||||
formatPeriod,
|
||||
currency,
|
||||
showDescriptions,
|
||||
}: {
|
||||
group: LineItemGroup;
|
||||
formatAmount: (amount: number, currency: string) => string;
|
||||
formatPeriod: (startMs: number | null, endMs: number | null) => string | null;
|
||||
currency: string;
|
||||
showDescriptions: boolean;
|
||||
}) {
|
||||
const isSingleItem = group.items.length === 1;
|
||||
const firstItem = group.items[0];
|
||||
@@ -359,19 +366,20 @@ function LineItemGroupRow({
|
||||
{/* Group header with label and total */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-col min-w-0 flex-1 gap-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm text-t1">{group.label}</span>
|
||||
{totalQuantity > 0 && (
|
||||
<Badge
|
||||
variant="muted"
|
||||
className="text-[10px] px-1.5 py-0 text-t3"
|
||||
>
|
||||
Qty: {totalQuantity}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{firstItem.description && (
|
||||
{showDescriptions ? (
|
||||
<span className="text-xs text-t3">{firstItem.description}</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm text-t1">{group.label}</span>
|
||||
{totalQuantity > 0 && (
|
||||
<Badge
|
||||
variant="muted"
|
||||
className="text-[10px] px-1.5 py-0 text-t3"
|
||||
>
|
||||
Qty: {totalQuantity}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{period && <span className="text-xs text-t4">{period}</span>}
|
||||
</div>
|
||||
@@ -380,6 +388,7 @@ function LineItemGroupRow({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Tier breakdown - each row has own hover */}
|
||||
<div className="mt-1 ml-3 flex flex-col gap-0.5">
|
||||
{group.items.map((item) => (
|
||||
<TierRow
|
||||
@@ -388,6 +397,7 @@ function LineItemGroupRow({
|
||||
hoverTexts={getLineItemHoverTexts(item)}
|
||||
formatAmount={formatAmount}
|
||||
currency={currency}
|
||||
showDescriptions={showDescriptions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -404,25 +414,26 @@ function LineItemGroupRow({
|
||||
<div className="flex flex-col py-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex flex-col min-w-0 flex-1 gap-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm text-t1">{group.label}</span>
|
||||
{(!group.isBasePrice && firstItem.total_quantity) ||
|
||||
(group.isBasePrice &&
|
||||
firstItem.stripe_quantity &&
|
||||
firstItem.stripe_quantity > 1) ? (
|
||||
<Badge
|
||||
variant="muted"
|
||||
className="text-[10px] px-1.5 py-0 text-t3"
|
||||
>
|
||||
Qty:{" "}
|
||||
{group.isBasePrice
|
||||
? firstItem.stripe_quantity
|
||||
: firstItem.total_quantity}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
{firstItem.description && (
|
||||
{showDescriptions ? (
|
||||
<span className="text-xs text-t3">{firstItem.description}</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm text-t1">{group.label}</span>
|
||||
{(!group.isBasePrice && firstItem.total_quantity) ||
|
||||
(group.isBasePrice &&
|
||||
firstItem.stripe_quantity &&
|
||||
firstItem.stripe_quantity > 1) ? (
|
||||
<Badge
|
||||
variant="muted"
|
||||
className="text-[10px] px-1.5 py-0 text-t3"
|
||||
>
|
||||
Qty:{" "}
|
||||
{group.isBasePrice
|
||||
? firstItem.stripe_quantity
|
||||
: firstItem.total_quantity}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{period && <span className="text-xs text-t4">{period}</span>}
|
||||
</div>
|
||||
@@ -472,18 +483,23 @@ function TierRow({
|
||||
hoverTexts,
|
||||
formatAmount,
|
||||
currency,
|
||||
showDescriptions,
|
||||
}: {
|
||||
item: InvoiceLineItem;
|
||||
hoverTexts: { key: string; value: string }[];
|
||||
formatAmount: (amount: number, currency: string) => string;
|
||||
currency: string;
|
||||
showDescriptions: boolean;
|
||||
}) {
|
||||
const isRefund = item.direction === "refund";
|
||||
const quantityLabel = item.total_quantity ? `${item.total_quantity}` : "";
|
||||
|
||||
return (
|
||||
<AdminHover asChild texts={hoverTexts}>
|
||||
<div className="flex items-center justify-between text-xs text-t3">
|
||||
<span>{item.description}</span>
|
||||
<span>
|
||||
{showDescriptions ? item.description : `${quantityLabel} units`}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"tabular-nums",
|
||||
|
||||
@@ -70,8 +70,6 @@ export function RefundInvoiceDialog({
|
||||
},
|
||||
});
|
||||
|
||||
const refundableAmount = invoice.amount_paid ?? invoice.total;
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (mode === "partial") {
|
||||
const parsed = Number.parseFloat(amount);
|
||||
@@ -79,8 +77,8 @@ export function RefundInvoiceDialog({
|
||||
toast.error("Please enter a valid refund amount");
|
||||
return;
|
||||
}
|
||||
if (parsed > refundableAmount) {
|
||||
toast.error("Refund amount cannot exceed the amount paid");
|
||||
if (parsed > invoice.total) {
|
||||
toast.error("Refund amount cannot exceed invoice total");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -88,7 +86,7 @@ export function RefundInvoiceDialog({
|
||||
};
|
||||
|
||||
const formattedTotal = formatAmount({
|
||||
amount: refundableAmount,
|
||||
amount: invoice.total,
|
||||
currency: invoice.currency,
|
||||
minFractionDigits: 2,
|
||||
amountFormatOptions: {
|
||||
@@ -102,7 +100,7 @@ export function RefundInvoiceDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>Refund Invoice</DialogTitle>
|
||||
<DialogDescription>
|
||||
Amount paid: {formattedTotal} {invoice.currency.toUpperCase()}
|
||||
Invoice total: {formattedTotal} {invoice.currency.toUpperCase()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -131,7 +129,7 @@ export function RefundInvoiceDialog({
|
||||
type="number"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
max={refundableAmount}
|
||||
max={invoice.total}
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
|
||||
@@ -24,17 +24,14 @@ const statusConfig = {
|
||||
};
|
||||
|
||||
const getRefundStatus = ({
|
||||
amountPaid,
|
||||
total,
|
||||
refundedAmount,
|
||||
}: {
|
||||
amountPaid: number;
|
||||
total: number;
|
||||
refundedAmount: number;
|
||||
}): { color: string; label: string } | null => {
|
||||
if (refundedAmount <= 0) return null;
|
||||
const refundableAmount = Math.abs(amountPaid ?? total);
|
||||
if (refundedAmount >= refundableAmount) {
|
||||
if (refundedAmount >= Math.abs(total)) {
|
||||
return {
|
||||
color: "bg-amber-500 dark:bg-amber-600",
|
||||
label: "Fully Refunded",
|
||||
@@ -48,12 +45,10 @@ const getRefundStatus = ({
|
||||
|
||||
export function CustomerInvoiceStatus({
|
||||
status,
|
||||
amountPaid,
|
||||
total,
|
||||
refundedAmount,
|
||||
}: {
|
||||
status: InvoiceStatus | null | undefined;
|
||||
amountPaid?: number | null;
|
||||
total?: number;
|
||||
refundedAmount?: number;
|
||||
}) {
|
||||
@@ -64,11 +59,7 @@ export function CustomerInvoiceStatus({
|
||||
status === InvoiceStatus.Paid &&
|
||||
total !== undefined &&
|
||||
refundedAmount !== undefined
|
||||
? getRefundStatus({
|
||||
amountPaid: amountPaid ?? total,
|
||||
total,
|
||||
refundedAmount,
|
||||
})
|
||||
? getRefundStatus({ total, refundedAmount })
|
||||
: null;
|
||||
|
||||
const config = refundStatus ?? statusConfig[status];
|
||||
|
||||
@@ -32,11 +32,10 @@ export const CustomerInvoicesColumns = [
|
||||
accessorKey: "total",
|
||||
cell: ({ row }: { row: Row<CustomerInvoice> }) => {
|
||||
const invoice = row.original;
|
||||
const displayTotal = invoice.amount_paid ?? invoice.total;
|
||||
const discountAmount = getTotalDiscountAmount(invoice);
|
||||
return (
|
||||
<div>
|
||||
{displayTotal.toFixed(2)} {invoice.currency.toUpperCase()}
|
||||
{invoice.total.toFixed(2)} {invoice.currency.toUpperCase()}
|
||||
{discountAmount > 0 && (
|
||||
<span className="text-t3"> (-{discountAmount.toFixed(2)})</span>
|
||||
)}
|
||||
@@ -52,7 +51,6 @@ export const CustomerInvoicesColumns = [
|
||||
return (
|
||||
<CustomerInvoiceStatus
|
||||
status={invoice.status}
|
||||
amountPaid={invoice.amount_paid}
|
||||
total={invoice.total}
|
||||
refundedAmount={invoice.refunded_amount}
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function CustomerInvoicesTable() {
|
||||
);
|
||||
|
||||
const { lineItemsByInvoiceId } = useInvoiceLineItemsQuery({
|
||||
customerId: customer?.id || customer?.internal_id,
|
||||
customerId: customer?.internal_id || customer?.id,
|
||||
invoiceIds,
|
||||
enabled: invoiceIds.length > 0,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Invoice, InvoiceLineItem } from "@autumn/shared";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { SheetContainer } from "@/components/v2/sheets/InlineSheet";
|
||||
import { SheetCloseButton } from "@/components/v2/sheets/SheetCloseButton";
|
||||
@@ -31,6 +32,7 @@ import { SHEET_ANIMATION } from "./customerAnimations";
|
||||
export function CustomerSheets() {
|
||||
const isMobile = useIsMobile();
|
||||
const sheetType = useSheetStore((s) => s.type);
|
||||
const sheetData = useSheetStore((s) => s.data);
|
||||
const closeSheet = useSheetStore((s) => s.closeSheet);
|
||||
const closeBalanceSheet = useCustomerBalanceSheetStore((s) => s.closeSheet);
|
||||
useSheetEscapeHandler();
|
||||
@@ -62,8 +64,12 @@ export function CustomerSheets() {
|
||||
return <BalanceDeleteSheet />;
|
||||
case "balance-create":
|
||||
return <BalanceCreateSheet />;
|
||||
case "invoice-detail":
|
||||
return <InvoiceDetailSheet />;
|
||||
case "invoice-detail": {
|
||||
const invoice = sheetData?.invoice as Invoice | undefined;
|
||||
const lineItems = (sheetData?.lineItems as InvoiceLineItem[]) ?? [];
|
||||
if (!invoice) return null;
|
||||
return <InvoiceDetailSheet invoice={invoice} lineItems={lineItems} />;
|
||||
}
|
||||
case "sync-stripe":
|
||||
return <SyncStripeSheet />;
|
||||
case "billing-auto-topup-add":
|
||||
|
||||
Reference in New Issue
Block a user