bug double subscription
This commit is contained in:
@@ -77,8 +77,12 @@ Versioned files in `database/migrations` update existing volumes. After pulling
|
||||
an update that includes a new migration, apply it once with:
|
||||
|
||||
```bash
|
||||
cat database/migrations/001_subscription_lifecycle.sql | docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U appspese -d appspese
|
||||
cat database/migrations/002_subscription_custom_intervals.sql | docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U appspese -d appspese
|
||||
cat database/migrations/001_subscription_lifecycle.sql | docker compose exec -T postgres sh -lc 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
|
||||
cat database/migrations/002_subscription_custom_intervals.sql | docker compose exec -T postgres sh -lc 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
|
||||
cat database/migrations/003_subscription_ledger_renewal_dates.sql | docker compose exec -T postgres sh -lc 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
|
||||
cat database/migrations/004_income_titles.sql | docker compose exec -T postgres sh -lc 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
|
||||
cat database/migrations/005_user_avatar.sql | docker compose exec -T postgres sh -lc 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
|
||||
cat database/migrations/006_subscription_payment_snapshots.sql | docker compose exec -T postgres sh -lc 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB"'
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -57,6 +57,8 @@ CREATE TABLE subscription_payments (
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subscription_id uuid NOT NULL REFERENCES subscriptions(id) ON DELETE CASCADE,
|
||||
renewal_date text NOT NULL,
|
||||
title text NOT NULL,
|
||||
amount double precision NOT NULL CHECK (amount > 0),
|
||||
status text NOT NULL,
|
||||
payment_date text,
|
||||
linked_payment_id uuid,
|
||||
|
||||
22
database/migrations/006_subscription_payment_snapshots.sql
Normal file
22
database/migrations/006_subscription_payment_snapshots.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
ALTER TABLE subscription_payments
|
||||
ADD COLUMN IF NOT EXISTS title text,
|
||||
ADD COLUMN IF NOT EXISTS amount double precision;
|
||||
|
||||
UPDATE subscription_payments AS payment
|
||||
SET
|
||||
title = subscription.title,
|
||||
amount = subscription.amount
|
||||
FROM subscriptions AS subscription
|
||||
WHERE
|
||||
subscription.id = payment.subscription_id
|
||||
AND (payment.title IS NULL OR payment.amount IS NULL);
|
||||
|
||||
ALTER TABLE subscription_payments
|
||||
ALTER COLUMN title SET NOT NULL,
|
||||
ALTER COLUMN amount SET NOT NULL;
|
||||
|
||||
ALTER TABLE subscription_payments
|
||||
DROP CONSTRAINT IF EXISTS subscription_payments_amount_positive;
|
||||
|
||||
ALTER TABLE subscription_payments
|
||||
ADD CONSTRAINT subscription_payments_amount_positive CHECK (amount > 0);
|
||||
@@ -146,6 +146,8 @@ export async function POST(request: Request) {
|
||||
collections.subscriptionPayments,
|
||||
{
|
||||
...data,
|
||||
title: subscription.title,
|
||||
amount: subscription.amount,
|
||||
userId,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
@@ -7,7 +7,16 @@ import {
|
||||
query,
|
||||
updateDocument,
|
||||
} from "@/lib/database/server";
|
||||
import { removeLinkedLedger } from "@/lib/finance/ledger";
|
||||
import { todayIso } from "@/lib/date/formatDate";
|
||||
import {
|
||||
removeLinkedLedger,
|
||||
syncSubscriptionLedger,
|
||||
} from "@/lib/finance/ledger";
|
||||
import {
|
||||
findClosestSubscriptionOccurrenceDate,
|
||||
isScheduledSubscriptionOccurrence,
|
||||
} from "@/lib/finance/subscriptionOccurrences";
|
||||
import { materializeSubscriptionHistory } from "@/lib/finance/subscriptionPaymentHistory";
|
||||
import { parseBody, updatedField } from "@/lib/validation/common";
|
||||
import { parseSubscriptionPatch } from "@/lib/validation/subscriptions";
|
||||
import type { Subscription, SubscriptionPayment } from "@/types/finance";
|
||||
@@ -36,6 +45,7 @@ export async function PATCH(request: Request, ctx: Ctx) {
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
await materializeSubscriptionHistory(current, "unpaid");
|
||||
const document = await updateDocument<Subscription>(
|
||||
collections.subscriptions,
|
||||
id,
|
||||
@@ -44,6 +54,82 @@ export async function PATCH(request: Request, ctx: Ctx) {
|
||||
...updatedField(),
|
||||
},
|
||||
);
|
||||
const payments = await listDocuments<SubscriptionPayment>(
|
||||
collections.subscriptionPayments,
|
||||
[
|
||||
query.equal("userId", auth.user.id),
|
||||
query.equal("subscriptionId", id),
|
||||
query.limit(5000),
|
||||
],
|
||||
);
|
||||
const occupiedDates = new Set(
|
||||
payments.documents.map((payment) => payment.renewalDate),
|
||||
);
|
||||
const today = todayIso();
|
||||
for (const payment of payments.documents) {
|
||||
if (payment.renewalDate <= today) continue;
|
||||
const renewalDate = findClosestSubscriptionOccurrenceDate(
|
||||
document,
|
||||
payment.renewalDate,
|
||||
);
|
||||
if (
|
||||
renewalDate &&
|
||||
renewalDate !== payment.renewalDate &&
|
||||
!occupiedDates.has(renewalDate)
|
||||
) {
|
||||
occupiedDates.delete(payment.renewalDate);
|
||||
occupiedDates.add(renewalDate);
|
||||
const updated = await updateDocument<SubscriptionPayment>(
|
||||
collections.subscriptionPayments,
|
||||
payment.$id,
|
||||
{
|
||||
renewalDate,
|
||||
title: document.title,
|
||||
amount: document.amount,
|
||||
...updatedField(),
|
||||
},
|
||||
);
|
||||
const ledger = await syncSubscriptionLedger(document, updated);
|
||||
await updateDocument<SubscriptionPayment>(
|
||||
collections.subscriptionPayments,
|
||||
payment.$id,
|
||||
{
|
||||
linkedPaymentId: ledger?.$id ?? "",
|
||||
...updatedField(),
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!isScheduledSubscriptionOccurrence(document, payment.renewalDate)) {
|
||||
if (payment.status === "paid") continue;
|
||||
await removeLinkedLedger(
|
||||
auth.user.id,
|
||||
payment.linkedPaymentId,
|
||||
"subscription",
|
||||
payment.$id,
|
||||
);
|
||||
await deleteDocument(collections.subscriptionPayments, payment.$id);
|
||||
continue;
|
||||
}
|
||||
const updated = await updateDocument<SubscriptionPayment>(
|
||||
collections.subscriptionPayments,
|
||||
payment.$id,
|
||||
{
|
||||
title: document.title,
|
||||
amount: document.amount,
|
||||
...updatedField(),
|
||||
},
|
||||
);
|
||||
const ledger = await syncSubscriptionLedger(document, updated);
|
||||
await updateDocument<SubscriptionPayment>(
|
||||
collections.subscriptionPayments,
|
||||
payment.$id,
|
||||
{
|
||||
linkedPaymentId: ledger?.$id ?? "",
|
||||
...updatedField(),
|
||||
},
|
||||
);
|
||||
}
|
||||
return Response.json(document);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
listDocuments,
|
||||
query,
|
||||
} from "@/lib/database/server";
|
||||
import { materializeSubscriptionHistory } from "@/lib/finance/subscriptionPaymentHistory";
|
||||
import { nowFields, parseBody } from "@/lib/validation/common";
|
||||
import { parseSubscriptionInput } from "@/lib/validation/subscriptions";
|
||||
import type { Subscription } from "@/types/finance";
|
||||
@@ -28,13 +29,17 @@ export async function POST(request: Request) {
|
||||
if (!parsed.ok)
|
||||
return Response.json({ error: parsed.error }, { status: 400 });
|
||||
const { collections } = databaseConfig();
|
||||
const { markPreviousAsPaid, ...subscription } = parsed.data;
|
||||
const document = await createDocument<Subscription>(
|
||||
collections.subscriptions,
|
||||
{
|
||||
...parsed.data,
|
||||
...subscription,
|
||||
userId: auth.user.id,
|
||||
...nowFields(),
|
||||
},
|
||||
);
|
||||
if (markPreviousAsPaid) {
|
||||
await materializeSubscriptionHistory(document, "paid");
|
||||
}
|
||||
return Response.json(document, { status: 201 });
|
||||
}
|
||||
|
||||
@@ -101,8 +101,13 @@ export function ReportsPageClient({ initialYear }: { initialYear: number }) {
|
||||
[annual, ledger.data, period, selectedYear],
|
||||
);
|
||||
const subscriptionInsights = useMemo(
|
||||
() => getSubscriptionInsights(subscriptions.data ?? [], period),
|
||||
[period, subscriptions.data],
|
||||
() =>
|
||||
getSubscriptionInsights(
|
||||
subscriptions.data ?? [],
|
||||
subscriptionPayments.data ?? [],
|
||||
period,
|
||||
),
|
||||
[period, subscriptionPayments.data, subscriptions.data],
|
||||
);
|
||||
const billsInsights = useMemo(
|
||||
() => getBillsInsights(bills.data ?? [], period),
|
||||
|
||||
@@ -61,6 +61,7 @@ export function SubscriptionForm({
|
||||
const [subscriptionStatus, setSubscriptionStatus] = useState(
|
||||
editing?.subscriptionStatus ?? "active",
|
||||
);
|
||||
const [markPreviousAsPaid, setMarkPreviousAsPaid] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
@@ -76,6 +77,7 @@ export function SubscriptionForm({
|
||||
setCustomIntervalValue(String(editing?.customIntervalValue ?? ""));
|
||||
setCustomIntervalUnit(editing?.customIntervalUnit ?? "months");
|
||||
setSubscriptionStatus(editing?.subscriptionStatus ?? "active");
|
||||
setMarkPreviousAsPaid(false);
|
||||
setError("");
|
||||
}, [editing]);
|
||||
|
||||
@@ -96,6 +98,7 @@ export function SubscriptionForm({
|
||||
customIntervalValue: billingCycle === "custom" ? customIntervalValue : "",
|
||||
customIntervalUnit: billingCycle === "custom" ? customIntervalUnit : "",
|
||||
subscriptionStatus,
|
||||
markPreviousAsPaid: !editing && markPreviousAsPaid,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,6 +196,9 @@ export function SubscriptionForm({
|
||||
billingCycle={billingCycle}
|
||||
durationMode={durationMode}
|
||||
editing={editing}
|
||||
firstPaymentDate={firstPaymentDate}
|
||||
markPreviousAsPaid={markPreviousAsPaid}
|
||||
onMarkPreviousAsPaidChange={setMarkPreviousAsPaid}
|
||||
onStatusChange={setSubscriptionStatus}
|
||||
status={subscriptionStatus}
|
||||
title={title}
|
||||
@@ -439,6 +445,9 @@ function ReviewStep({
|
||||
billingCycle,
|
||||
durationMode,
|
||||
editing,
|
||||
firstPaymentDate,
|
||||
markPreviousAsPaid,
|
||||
onMarkPreviousAsPaidChange,
|
||||
status,
|
||||
onStatusChange,
|
||||
}: {
|
||||
@@ -447,6 +456,9 @@ function ReviewStep({
|
||||
billingCycle: BillingCycle;
|
||||
durationMode: DurationMode;
|
||||
editing?: Subscription;
|
||||
firstPaymentDate: string;
|
||||
markPreviousAsPaid: boolean;
|
||||
onMarkPreviousAsPaidChange: (value: boolean) => void;
|
||||
status: SubscriptionStatus;
|
||||
onStatusChange: (value: SubscriptionStatus) => void;
|
||||
}) {
|
||||
@@ -478,9 +490,32 @@ function ReviewStep({
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-slate-400">
|
||||
Il nuovo abbonamento sarà creato come attivo.
|
||||
</p>
|
||||
<>
|
||||
<p className="text-sm text-slate-400">
|
||||
Il nuovo abbonamento sarà creato come attivo.
|
||||
</p>
|
||||
{firstPaymentDate < todayIso() ? (
|
||||
<label className="flex cursor-pointer items-start gap-3 rounded-lg border border-white/10 bg-slate-950/20 p-4 text-sm text-slate-200">
|
||||
<input
|
||||
checked={markPreviousAsPaid}
|
||||
className="mt-0.5 size-4 accent-sky-500"
|
||||
onChange={(event) =>
|
||||
onMarkPreviousAsPaidChange(event.target.checked)
|
||||
}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span className="block font-medium text-white">
|
||||
Segna le rate precedenti come pagate
|
||||
</span>
|
||||
<span className="mt-1 block text-xs leading-relaxed text-slate-400">
|
||||
Crea la cronologia dei rinnovi precedenti a oggi e li registra
|
||||
come pagati.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -89,6 +89,9 @@ export function SubscriptionsPageClient() {
|
||||
? list.map((item) => (item.$id === saved.$id ? saved : item))
|
||||
: [saved, ...list];
|
||||
});
|
||||
if (editing) {
|
||||
await subscriptionPayments.mutate();
|
||||
}
|
||||
setModalOpen(false);
|
||||
setEditing(undefined);
|
||||
notify("Abbonamento salvato");
|
||||
|
||||
@@ -50,6 +50,8 @@ const tableFields = {
|
||||
"userId",
|
||||
"subscriptionId",
|
||||
"renewalDate",
|
||||
"title",
|
||||
"amount",
|
||||
"status",
|
||||
"paymentDate",
|
||||
"linkedPaymentId",
|
||||
|
||||
@@ -193,8 +193,8 @@ export async function syncSubscriptionLedger(
|
||||
}
|
||||
return upsertLedgerForSource({
|
||||
userId: payment.userId,
|
||||
title: subscription.title,
|
||||
amount: subscription.amount,
|
||||
title: payment.title ?? subscription.title,
|
||||
amount: payment.amount ?? subscription.amount,
|
||||
date: payment.renewalDate,
|
||||
sourceType: "subscription",
|
||||
sourceId: payment.$id,
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
Income,
|
||||
PaymentLedger,
|
||||
Subscription,
|
||||
SubscriptionPayment,
|
||||
} from "@/types/finance";
|
||||
|
||||
export function getMonthlyReportSummary(input: {
|
||||
@@ -21,17 +22,26 @@ export function getMonthlyReportSummary(input: {
|
||||
|
||||
export function getSubscriptionInsights(
|
||||
subscriptions: Subscription[],
|
||||
payments: SubscriptionPayment[],
|
||||
period: number | string = new Date().getFullYear(),
|
||||
) {
|
||||
const { start, end } = periodBounds(period);
|
||||
return subscriptions
|
||||
.filter((item) => item.subscriptionStatus === "active")
|
||||
.map((subscription) => ({
|
||||
subscription,
|
||||
periodCost:
|
||||
generateSubscriptionOccurrencesForRange([subscription], [], start, end)
|
||||
.length * subscription.amount,
|
||||
}))
|
||||
.map((subscription) => {
|
||||
const occurrences = generateSubscriptionOccurrencesForRange(
|
||||
[subscription],
|
||||
payments,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
return {
|
||||
subscription,
|
||||
periodCost: occurrences.reduce(
|
||||
(total, occurrence) => total + occurrence.subscription.amount,
|
||||
0,
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((item) => item.periodCost > 0)
|
||||
.sort((a, b) => b.periodCost - a.periodCost);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
addDays,
|
||||
addMonths,
|
||||
addYears,
|
||||
fromIso,
|
||||
monthBounds,
|
||||
todayIso,
|
||||
} from "@/lib/date/formatDate";
|
||||
@@ -48,7 +49,7 @@ export function generateSubscriptionOccurrencesForRange(
|
||||
for (const payment of paymentsBySubscription.get(subscription.$id) ?? []) {
|
||||
if (payment.renewalDate < start || payment.renewalDate > end) continue;
|
||||
occurrences.set(occurrenceKey(subscription.$id, payment.renewalDate), {
|
||||
subscription,
|
||||
subscription: subscriptionSnapshot(subscription, payment),
|
||||
renewalDate: payment.renewalDate,
|
||||
payment,
|
||||
status: resolveSubscriptionOccurrenceStatus(
|
||||
@@ -64,6 +65,12 @@ export function generateSubscriptionOccurrencesForRange(
|
||||
occurrence.payment ??= paymentsBySubscription
|
||||
.get(occurrence.subscription.$id)
|
||||
?.find((payment) => payment.renewalDate === occurrence.renewalDate);
|
||||
if (occurrence.payment) {
|
||||
occurrence.subscription = subscriptionSnapshot(
|
||||
occurrence.subscription,
|
||||
occurrence.payment,
|
||||
);
|
||||
}
|
||||
occurrence.status = resolveSubscriptionOccurrenceStatus(
|
||||
occurrence.renewalDate,
|
||||
occurrence.payment,
|
||||
@@ -76,6 +83,17 @@ export function generateSubscriptionOccurrencesForRange(
|
||||
);
|
||||
}
|
||||
|
||||
function subscriptionSnapshot(
|
||||
subscription: Subscription,
|
||||
payment: SubscriptionPayment,
|
||||
) {
|
||||
return {
|
||||
...subscription,
|
||||
title: payment.title ?? subscription.title,
|
||||
amount: payment.amount ?? subscription.amount,
|
||||
};
|
||||
}
|
||||
|
||||
export function isScheduledSubscriptionOccurrence(
|
||||
subscription: Subscription,
|
||||
renewalDate: string,
|
||||
@@ -89,6 +107,30 @@ export function isScheduledSubscriptionOccurrence(
|
||||
).some((occurrence) => occurrence.renewalDate === renewalDate);
|
||||
}
|
||||
|
||||
export function findClosestSubscriptionOccurrenceDate(
|
||||
subscription: Subscription,
|
||||
renewalDate: string,
|
||||
) {
|
||||
if (subscription.subscriptionStatus !== "active") return undefined;
|
||||
let step = 0;
|
||||
let closest: string | undefined;
|
||||
let closestDistance = Number.POSITIVE_INFINITY;
|
||||
while (step < MAX_GENERATED_OCCURRENCES) {
|
||||
const candidate = occurrenceDate(subscription, step);
|
||||
if (!hasOccurrence(subscription, candidate, step)) break;
|
||||
const distance = Math.abs(
|
||||
fromIso(candidate).getTime() - fromIso(renewalDate).getTime(),
|
||||
);
|
||||
if (distance < closestDistance) {
|
||||
closest = candidate;
|
||||
closestDistance = distance;
|
||||
}
|
||||
if (candidate >= renewalDate) break;
|
||||
step += 1;
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
export function getEffectiveSubscriptionStatus(
|
||||
subscription: Subscription,
|
||||
referenceDate = todayIso(),
|
||||
|
||||
69
src/lib/finance/subscriptionPaymentHistory.ts
Normal file
69
src/lib/finance/subscriptionPaymentHistory.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import "server-only";
|
||||
|
||||
import {
|
||||
createDocument,
|
||||
databaseConfig,
|
||||
listDocuments,
|
||||
query,
|
||||
updateDocument,
|
||||
} from "@/lib/database/server";
|
||||
import { addDays, todayIso } from "@/lib/date/formatDate";
|
||||
import { syncSubscriptionLedger } from "@/lib/finance/ledger";
|
||||
import { generateSubscriptionOccurrencesForRange } from "@/lib/finance/subscriptionOccurrences";
|
||||
import { nowFields, updatedField } from "@/lib/validation/common";
|
||||
import type { Subscription, SubscriptionPayment } from "@/types/finance";
|
||||
|
||||
export async function materializeSubscriptionHistory(
|
||||
subscription: Subscription,
|
||||
status: SubscriptionPayment["status"],
|
||||
beforeDate = todayIso(),
|
||||
) {
|
||||
const end = addDays(beforeDate, -1);
|
||||
if (end < subscription.firstPaymentDate) return [];
|
||||
const { collections } = databaseConfig();
|
||||
const stored = await listDocuments<SubscriptionPayment>(
|
||||
collections.subscriptionPayments,
|
||||
[
|
||||
query.equal("userId", subscription.userId),
|
||||
query.equal("subscriptionId", subscription.$id),
|
||||
query.limit(5000),
|
||||
],
|
||||
);
|
||||
const storedDates = new Set(
|
||||
stored.documents.map((payment) => payment.renewalDate),
|
||||
);
|
||||
const scheduled = generateSubscriptionOccurrencesForRange(
|
||||
[{ ...subscription, subscriptionStatus: "active" }],
|
||||
[],
|
||||
subscription.firstPaymentDate,
|
||||
end,
|
||||
);
|
||||
const created: SubscriptionPayment[] = [];
|
||||
for (const occurrence of scheduled) {
|
||||
if (storedDates.has(occurrence.renewalDate)) continue;
|
||||
let payment = await createDocument<SubscriptionPayment>(
|
||||
collections.subscriptionPayments,
|
||||
{
|
||||
userId: subscription.userId,
|
||||
subscriptionId: subscription.$id,
|
||||
renewalDate: occurrence.renewalDate,
|
||||
title: subscription.title,
|
||||
amount: subscription.amount,
|
||||
status,
|
||||
paymentDate: status === "paid" ? occurrence.renewalDate : undefined,
|
||||
...nowFields(),
|
||||
},
|
||||
);
|
||||
const ledger = await syncSubscriptionLedger(subscription, payment);
|
||||
payment = await updateDocument<SubscriptionPayment>(
|
||||
collections.subscriptionPayments,
|
||||
payment.$id,
|
||||
{
|
||||
linkedPaymentId: ledger?.$id ?? "",
|
||||
...updatedField(),
|
||||
},
|
||||
);
|
||||
created.push(payment);
|
||||
}
|
||||
return created;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const intervalUnits: RenewalIntervalUnit[] = [
|
||||
"years",
|
||||
];
|
||||
|
||||
export function parseSubscriptionInput(body: Record<string, unknown>) {
|
||||
function parseSubscriptionFields(body: Record<string, unknown>) {
|
||||
const amount = validateAmount(body.amount);
|
||||
if (!amount) throw new Error("L'importo deve essere maggiore di zero");
|
||||
const activationDate = requireDate(body.activationDate, "activationDate");
|
||||
@@ -65,8 +65,15 @@ export function parseSubscriptionInput(body: Record<string, unknown>) {
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSubscriptionInput(body: Record<string, unknown>) {
|
||||
return {
|
||||
...parseSubscriptionFields(body),
|
||||
markPreviousAsPaid: body.markPreviousAsPaid === true,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSubscriptionPatch(body: Record<string, unknown>) {
|
||||
return parseSubscriptionInput(body);
|
||||
return parseSubscriptionFields(body);
|
||||
}
|
||||
|
||||
function optionalDate(value: unknown, name: string) {
|
||||
|
||||
@@ -47,6 +47,8 @@ export type Subscription = BaseDocument & {
|
||||
export type SubscriptionPayment = BaseDocument & {
|
||||
subscriptionId: string;
|
||||
renewalDate: string;
|
||||
title: string;
|
||||
amount: number;
|
||||
status: "paid" | "unpaid";
|
||||
paymentDate?: string;
|
||||
linkedPaymentId?: string;
|
||||
|
||||
Reference in New Issue
Block a user