mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fixes
This commit is contained in:
@@ -151,6 +151,15 @@ describe("BillingService.markInvoiceAsPaid", () => {
|
||||
paidAt: expect.any(Date),
|
||||
paidAmount: 1500,
|
||||
balanceAmount: 0,
|
||||
payments: [
|
||||
{
|
||||
amount: 1500,
|
||||
method: "GATEWAY",
|
||||
reference: "pay-1",
|
||||
paidAt: expect.any(String),
|
||||
metadata: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
expect(events.emit).toHaveBeenCalledWith(
|
||||
@@ -198,8 +207,14 @@ describe("BillingService.recordPayment", () => {
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const events = makeEvents();
|
||||
const dataSource = {
|
||||
manager: mg,
|
||||
transaction: jest
|
||||
.fn()
|
||||
.mockImplementation((cb: (mg: unknown) => unknown) => cb(mg)),
|
||||
};
|
||||
const service = new BillingService(
|
||||
{ manager: mg } as never,
|
||||
dataSource as never,
|
||||
{} as never,
|
||||
{} as never,
|
||||
events as never,
|
||||
@@ -265,6 +280,14 @@ describe("BillingService.recordPayment", () => {
|
||||
expect(mg.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a payment that exceeds the outstanding balance", async () => {
|
||||
const { service, mg } = serviceFor(openInvoice());
|
||||
await expect(
|
||||
service.recordPayment("inv-1", { amount: 1500 }),
|
||||
).rejects.toThrow();
|
||||
expect(mg.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects payment against a cancelled invoice", async () => {
|
||||
const { service, mg } = serviceFor(
|
||||
openInvoice({ status: Freight.InvoiceStatus.Cancelled }),
|
||||
|
||||
@@ -415,40 +415,90 @@ export class BillingService {
|
||||
|
||||
// ── State transitions ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Run `fn` inside a transaction and only emit its returned domain event
|
||||
* after commit. When the caller passes their own `manager`, they own commit
|
||||
* timing — `fn`'s event fires inline as soon as it resolves (the outer
|
||||
* transaction may still roll back afterwards; this is the caller's
|
||||
* documented tradeoff). When no `manager` is given, this opens its own
|
||||
* transaction and defers the emit until after that transaction commits, so
|
||||
* listeners (e.g. booking advancement) can never observe an invoice change
|
||||
* that then rolls back.
|
||||
*/
|
||||
private async runTransition<T>(
|
||||
manager: EntityManager | undefined,
|
||||
fn: (mg: EntityManager) => Promise<{ result: T; emit?: () => void }>,
|
||||
): Promise<T> {
|
||||
if (manager) {
|
||||
const { result, emit } = await fn(manager);
|
||||
emit?.();
|
||||
return result;
|
||||
}
|
||||
let pending: (() => void) | undefined;
|
||||
const result = await this.dataSource.transaction(async (mg) => {
|
||||
const out = await fn(mg);
|
||||
pending = out.emit;
|
||||
return out.result;
|
||||
});
|
||||
pending?.();
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an invoice paid, stamp the paid timestamp, sync paid/balance amounts,
|
||||
* link the gateway payment, then emit `${source}.invoice.paid`. Full-payment
|
||||
* only — no partial settlement. No-op when the invoice is already paid.
|
||||
* Pass `manager` to enlist in a caller's transaction.
|
||||
* append the settlement to the `payments` ledger, link the gateway payment,
|
||||
* then emit `${source}.invoice.paid`. Full-payment only — no partial
|
||||
* settlement. No-op when the invoice is already paid. Pass `manager` to
|
||||
* enlist in a caller's transaction; otherwise locks the row for update and
|
||||
* emits only after commit (see {@link runTransition}).
|
||||
*/
|
||||
async markInvoiceAsPaid(
|
||||
invoiceId: string,
|
||||
paymentId: string | null = null,
|
||||
manager?: EntityManager,
|
||||
settlement: { providerTxnId?: string; paidAt?: Date } = {},
|
||||
): Promise<Invoice | null> {
|
||||
const mg = manager ?? this.dataSource.manager;
|
||||
const invoice = await mg.findOne(Invoice, { where: { id: invoiceId } });
|
||||
if (!invoice) throw new NotFoundException(`Invoice ${invoiceId} not found`);
|
||||
if (invoice.status === Freight.InvoiceStatus.Paid) return invoice;
|
||||
return this.runTransition(manager, async (mg) => {
|
||||
const invoice = await mg.findOne(Invoice, {
|
||||
where: { id: invoiceId },
|
||||
lock: { mode: "pessimistic_write" },
|
||||
});
|
||||
if (!invoice) {
|
||||
throw new NotFoundException(`Invoice ${invoiceId} not found`);
|
||||
}
|
||||
if (invoice.status === Freight.InvoiceStatus.Paid) {
|
||||
return { result: invoice };
|
||||
}
|
||||
|
||||
await mg.update(Invoice, { id: invoiceId }, {
|
||||
status: Freight.InvoiceStatus.Paid,
|
||||
paymentId,
|
||||
paidAt: invoice.paidAt ?? new Date(),
|
||||
paidAmount: invoice.totalAmount,
|
||||
balanceAmount: 0,
|
||||
} as never);
|
||||
const paidAt = invoice.paidAt ?? settlement.paidAt ?? new Date();
|
||||
const settledAmount = round2(
|
||||
Number(invoice.totalAmount) - Number(invoice.paidAmount ?? 0),
|
||||
);
|
||||
const entry: InvoicePayment = {
|
||||
amount: settledAmount,
|
||||
method: "GATEWAY",
|
||||
reference: settlement.providerTxnId ?? paymentId ?? null,
|
||||
paidAt: paidAt.toISOString(),
|
||||
metadata: null,
|
||||
};
|
||||
const payments = [...(invoice.payments ?? []), entry];
|
||||
|
||||
const updated = {
|
||||
...invoice,
|
||||
status: Freight.InvoiceStatus.Paid,
|
||||
paymentId,
|
||||
paidAt: invoice.paidAt ?? new Date(),
|
||||
paidAmount: invoice.totalAmount,
|
||||
balanceAmount: 0,
|
||||
} as Invoice;
|
||||
this.emitInvoiceEvent("paid", updated);
|
||||
return updated;
|
||||
const patch = {
|
||||
status: Freight.InvoiceStatus.Paid,
|
||||
paymentId,
|
||||
paidAt,
|
||||
paidAmount: invoice.totalAmount,
|
||||
balanceAmount: 0,
|
||||
payments,
|
||||
};
|
||||
await mg.update(Invoice, { id: invoiceId }, patch as never);
|
||||
|
||||
const updated = { ...invoice, ...patch } as Invoice;
|
||||
return {
|
||||
result: updated,
|
||||
emit: () => this.emitInvoiceEvent("paid", updated),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -460,9 +510,11 @@ export class BillingService {
|
||||
* at the warehouse counter); gateway settlement goes through
|
||||
* {@link markInvoiceAsPaid}.
|
||||
*
|
||||
* Throws when the invoice is missing, cancelled, refunded, already fully paid,
|
||||
* or when `amount` is not positive. Pass `manager` to enlist in a caller's
|
||||
* transaction.
|
||||
* Throws when the invoice is missing, cancelled, refunded, already fully
|
||||
* paid, `amount` is not positive, or `amount` exceeds the outstanding
|
||||
* balance. Pass `manager` to enlist in a caller's transaction; otherwise
|
||||
* locks the row for update and emits only after commit (see
|
||||
* {@link runTransition}).
|
||||
*/
|
||||
async recordPayment(
|
||||
invoiceId: string,
|
||||
@@ -475,62 +527,71 @@ export class BillingService {
|
||||
);
|
||||
}
|
||||
|
||||
const mg = manager ?? this.dataSource.manager;
|
||||
const invoice = await mg.findOne(Invoice, { where: { id: invoiceId } });
|
||||
if (!invoice) throw new NotFoundException(`Invoice ${invoiceId} not found`);
|
||||
if (invoice.status === Freight.InvoiceStatus.Cancelled) {
|
||||
throw new BadRequestException("Cannot pay a cancelled invoice.");
|
||||
}
|
||||
if (invoice.status === Freight.InvoiceStatus.Refunded) {
|
||||
throw new BadRequestException("Cannot pay a refunded invoice.");
|
||||
}
|
||||
if (invoice.status === Freight.InvoiceStatus.Paid) {
|
||||
throw new BadRequestException("Invoice is already fully paid.");
|
||||
}
|
||||
return this.runTransition(manager, async (mg) => {
|
||||
const invoice = await mg.findOne(Invoice, {
|
||||
where: { id: invoiceId },
|
||||
lock: { mode: "pessimistic_write" },
|
||||
});
|
||||
if (!invoice) {
|
||||
throw new NotFoundException(`Invoice ${invoiceId} not found`);
|
||||
}
|
||||
if (invoice.status === Freight.InvoiceStatus.Cancelled) {
|
||||
throw new BadRequestException("Cannot pay a cancelled invoice.");
|
||||
}
|
||||
if (invoice.status === Freight.InvoiceStatus.Refunded) {
|
||||
throw new BadRequestException("Cannot pay a refunded invoice.");
|
||||
}
|
||||
if (invoice.status === Freight.InvoiceStatus.Paid) {
|
||||
throw new BadRequestException("Invoice is already fully paid.");
|
||||
}
|
||||
if (round2(input.amount) > Number(invoice.balanceAmount)) {
|
||||
throw new BadRequestException(
|
||||
`Payment of ${round2(input.amount)} exceeds the outstanding balance of ${Number(invoice.balanceAmount)}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const at = input.paidAt ?? new Date();
|
||||
const { paidAmount, balanceAmount, fullyPaid } = applySettlement(
|
||||
invoice.totalAmount,
|
||||
invoice.paidAmount,
|
||||
input.amount,
|
||||
);
|
||||
const status = fullyPaid
|
||||
? Freight.InvoiceStatus.Paid
|
||||
: Freight.InvoiceStatus.PartiallyPaid;
|
||||
const at = input.paidAt ?? new Date();
|
||||
const { paidAmount, balanceAmount, fullyPaid } = applySettlement(
|
||||
invoice.totalAmount,
|
||||
invoice.paidAmount,
|
||||
input.amount,
|
||||
);
|
||||
const status = fullyPaid
|
||||
? Freight.InvoiceStatus.Paid
|
||||
: Freight.InvoiceStatus.PartiallyPaid;
|
||||
|
||||
const entry: InvoicePayment = {
|
||||
amount: round2(input.amount),
|
||||
method: input.method ?? null,
|
||||
reference: input.reference ?? null,
|
||||
paidAt: at.toISOString(),
|
||||
metadata: input.metadata ?? null,
|
||||
};
|
||||
const payments = [...(invoice.payments ?? []), entry];
|
||||
const entry: InvoicePayment = {
|
||||
amount: round2(input.amount),
|
||||
method: input.method ?? null,
|
||||
reference: input.reference ?? null,
|
||||
paidAt: at.toISOString(),
|
||||
metadata: input.metadata ?? null,
|
||||
};
|
||||
const payments = [...(invoice.payments ?? []), entry];
|
||||
|
||||
await mg.update(Invoice, { id: invoice.id }, {
|
||||
paidAmount,
|
||||
balanceAmount,
|
||||
status,
|
||||
payments,
|
||||
paidAt: fullyPaid ? at : (invoice.paidAt ?? null),
|
||||
} as never);
|
||||
const patch = {
|
||||
paidAmount,
|
||||
balanceAmount,
|
||||
status,
|
||||
payments,
|
||||
paidAt: fullyPaid ? at : (invoice.paidAt ?? null),
|
||||
};
|
||||
await mg.update(Invoice, { id: invoice.id }, patch as never);
|
||||
|
||||
const updated = {
|
||||
...invoice,
|
||||
paidAmount,
|
||||
balanceAmount,
|
||||
status,
|
||||
payments,
|
||||
paidAt: fullyPaid ? at : (invoice.paidAt ?? null),
|
||||
} as Invoice;
|
||||
|
||||
if (fullyPaid) this.emitInvoiceEvent("paid", updated);
|
||||
return updated;
|
||||
const updated = { ...invoice, ...patch } as Invoice;
|
||||
return {
|
||||
result: updated,
|
||||
emit: fullyPaid
|
||||
? () => this.emitInvoiceEvent("paid", updated)
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an invoice refunded and emit `${source}.invoice.refunded`.
|
||||
* No-op when already refunded.
|
||||
* No-op when already refunded. Throws when the invoice has no recorded
|
||||
* payment (nothing to refund).
|
||||
*/
|
||||
async markInvoiceAsRefunded(
|
||||
invoiceId: string,
|
||||
@@ -542,12 +603,20 @@ export class BillingService {
|
||||
"refunded",
|
||||
{},
|
||||
manager,
|
||||
(invoice) => {
|
||||
if (!(Number(invoice.paidAmount) > 0)) {
|
||||
throw new BadRequestException(
|
||||
"Cannot refund an invoice with no recorded payment.",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an invoice cancelled and emit `${source}.invoice.cancelled`.
|
||||
* No-op when already cancelled.
|
||||
* No-op when already cancelled. Throws when the invoice has payments
|
||||
* recorded against it (refund it instead).
|
||||
*/
|
||||
async cancelInvoice(
|
||||
invoiceId: string,
|
||||
@@ -559,16 +628,23 @@ export class BillingService {
|
||||
"cancelled",
|
||||
{},
|
||||
manager,
|
||||
(invoice) => {
|
||||
if (Number(invoice.paidAmount) > 0) {
|
||||
throw new BadRequestException(
|
||||
"Cannot cancel an invoice that has payments recorded against it.",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the invoice, apply the new status (+ extra columns), then emit
|
||||
* `${source}.invoice.<event>`. No-op (returns the invoice) when it is already
|
||||
* in the target status. Throws when the invoice does not exist.
|
||||
*
|
||||
* Note: the event fires in-process synchronously. When a `manager` from an
|
||||
* outer transaction is passed, listeners run before that transaction commits.
|
||||
* `${source}.invoice.<event>`. No-op (returns the invoice, skipping `guard`)
|
||||
* when it is already in the target status. Throws when the invoice does not
|
||||
* exist or `guard` rejects the current state. Pass `manager` to enlist in a
|
||||
* caller's transaction; otherwise locks the row for update and emits only
|
||||
* after commit (see {@link runTransition}).
|
||||
*/
|
||||
private async transition(
|
||||
invoiceId: string,
|
||||
@@ -576,17 +652,27 @@ export class BillingService {
|
||||
event: string,
|
||||
extra: { paymentId?: string },
|
||||
manager?: EntityManager,
|
||||
guard?: (invoice: Invoice) => void,
|
||||
): Promise<Invoice | null> {
|
||||
const mg = manager ?? this.dataSource.manager;
|
||||
const invoice = await mg.findOne(Invoice, { where: { id: invoiceId } });
|
||||
if (!invoice) throw new NotFoundException(`Invoice ${invoiceId} not found`);
|
||||
if (invoice.status === status) return invoice;
|
||||
return this.runTransition(manager, async (mg) => {
|
||||
const invoice = await mg.findOne(Invoice, {
|
||||
where: { id: invoiceId },
|
||||
lock: { mode: "pessimistic_write" },
|
||||
});
|
||||
if (!invoice) {
|
||||
throw new NotFoundException(`Invoice ${invoiceId} not found`);
|
||||
}
|
||||
if (invoice.status === status) return { result: invoice };
|
||||
guard?.(invoice);
|
||||
|
||||
await mg.update(Invoice, { id: invoice.id }, { status, ...extra });
|
||||
await mg.update(Invoice, { id: invoice.id }, { status, ...extra });
|
||||
|
||||
const updated = { ...invoice, ...extra, status } as Invoice;
|
||||
this.emitInvoiceEvent(event, updated);
|
||||
return updated;
|
||||
const updated = { ...invoice, ...extra, status } as Invoice;
|
||||
return {
|
||||
result: updated,
|
||||
emit: () => this.emitInvoiceEvent(event, updated),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Broadcast `${invoice.source}.invoice.<event>` to in-process listeners. */
|
||||
@@ -789,7 +875,9 @@ export class BillingService {
|
||||
// service branches on a domain-specific reference type.
|
||||
referenceType: PaymentReferenceType.SHIPMENT,
|
||||
orderRef: invoice.invoiceNumber,
|
||||
amountMinor: Math.round(Number(invoice.balanceAmount)),
|
||||
// True minor units (cents) — every provider adapter divides by 100 to
|
||||
// get the major amount it charges.
|
||||
amountMinor: Math.round(Number(invoice.balanceAmount) * 100),
|
||||
currency: invoice.currency,
|
||||
reason: `Payment for invoice ${invoice.invoiceNumber}`,
|
||||
method: opts.method ?? "TELEBIRR",
|
||||
@@ -824,8 +912,8 @@ export class BillingService {
|
||||
*/
|
||||
async settleByPaymentId(
|
||||
paymentId: string,
|
||||
_providerTxnId?: string,
|
||||
_paidAt?: Date,
|
||||
providerTxnId?: string,
|
||||
paidAt?: Date,
|
||||
): Promise<Invoice | null> {
|
||||
const invoice = await this.dataSource.getRepository(Invoice).findOne({
|
||||
where: { paymentId, status: In(OPEN_STATUSES) },
|
||||
@@ -833,6 +921,9 @@ export class BillingService {
|
||||
});
|
||||
if (!invoice) return null;
|
||||
|
||||
return this.markInvoiceAsPaid(invoice.id, paymentId);
|
||||
return this.markInvoiceAsPaid(invoice.id, paymentId, undefined, {
|
||||
providerTxnId,
|
||||
paidAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
/** Anything exposing TypeORM's `.query` — an `EntityManager` or `DataSource`. */
|
||||
export interface SqlRunner {
|
||||
query(sql: string, params?: unknown[]): Promise<Array<{ seq: number | string }>>;
|
||||
query(sql: string, params?: unknown[]): Promise<unknown>;
|
||||
}
|
||||
|
||||
export interface InvoiceNumberOptions {
|
||||
@@ -34,11 +34,18 @@ export async function nextDailyInvoiceNumber(
|
||||
const prefix = `${opts.code}-${ymd}-`;
|
||||
const column = opts.column ?? "invoice_number";
|
||||
|
||||
const [row] = await runner.query(
|
||||
// Serialize concurrent allocation for this exact day+code prefix so two
|
||||
// simultaneous transactions can't both read the same MAX(seq) and mint a
|
||||
// duplicate number. Session-scoped to the caller's transaction — released
|
||||
// automatically on commit/rollback. Different prefixes hash to different
|
||||
// keys and never contend with each other.
|
||||
await runner.query(`SELECT pg_advisory_xact_lock(hashtext($1))`, [prefix]);
|
||||
|
||||
const rows = (await runner.query(
|
||||
`SELECT COALESCE(MAX(CAST(split_part(${column}, '-', 3) AS int)), 0) AS seq
|
||||
FROM ${opts.table} WHERE ${column} LIKE $1`,
|
||||
[`${prefix}%`],
|
||||
);
|
||||
const next = Number(row?.seq ?? 0) + 1;
|
||||
)) as Array<{ seq: number | string }>;
|
||||
const next = Number(rows[0]?.seq ?? 0) + 1;
|
||||
return `${prefix}${String(next).padStart(5, "0")}`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common";
|
||||
import {
|
||||
BadRequestException,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { OnEvent } from "@nestjs/event-emitter";
|
||||
import { Freight } from "@edr/types";
|
||||
import { DataSource } from "typeorm";
|
||||
import { DataSource, EntityManager } from "typeorm";
|
||||
|
||||
import {
|
||||
BillingService,
|
||||
@@ -58,9 +64,9 @@ export class BookingInvoiceService {
|
||||
* Ensure the booking has its invoice, generating one from the snapshotted
|
||||
* pricing breakdown if absent. Called when a booking reaches a billable state.
|
||||
* Idempotent — returns the existing open invoice instead of a duplicate.
|
||||
* Returns `null` (and logs) when the booking is not billable: no company to
|
||||
* bill (e.g. government bookings whose `companyId` is null, which the invoices
|
||||
* FK requires), or no priced amount.
|
||||
* Throws `BadRequestException` when the booking is not billable: no company
|
||||
* to bill (e.g. government bookings whose `companyId` is null, which the
|
||||
* invoices FK requires), or no priced amount.
|
||||
*/
|
||||
async ensureInvoiceForBooking(
|
||||
booking: Booking,
|
||||
@@ -74,8 +80,8 @@ export class BookingInvoiceService {
|
||||
if (existing) return existing;
|
||||
|
||||
if (!booking.companyId) {
|
||||
this.logger.warn(
|
||||
`Skipping invoice for booking ${booking.reference} (${booking.id}): no company to bill.`,
|
||||
throw new BadRequestException(
|
||||
`Cannot generate invoice for booking ${booking.reference} (${booking.id}): no company to bill.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +108,13 @@ export class BookingInvoiceService {
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus = this.billing.updateStatus;
|
||||
updateStatus(
|
||||
invoiceId: string,
|
||||
status: Freight.InvoiceStatus,
|
||||
manager?: EntityManager,
|
||||
): Promise<void> {
|
||||
return this.billing.updateStatus(invoiceId, status, manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance a booking once its prepaid invoice settles — the domain side-effect
|
||||
@@ -165,7 +177,11 @@ export class BookingInvoiceService {
|
||||
// Fall back to a single freight line when no breakdown was snapshotted.
|
||||
if (lines.length === 0) {
|
||||
const amount = Number(booking.totalAmount);
|
||||
if (!Number.isFinite(amount) || amount <= 0) throw new Error("No price");
|
||||
if (!Number.isFinite(amount) || amount <= 0) {
|
||||
throw new BadRequestException(
|
||||
`Cannot generate invoice for booking ${booking.reference} (${booking.id}): no priced amount.`,
|
||||
);
|
||||
}
|
||||
lines.push({
|
||||
chargeType: "FREIGHT",
|
||||
description: "Rail freight",
|
||||
|
||||
@@ -48,7 +48,7 @@ export class FirstMileInvoiceService {
|
||||
}
|
||||
|
||||
// Fetch the booking to get the companyId and companyProfileId
|
||||
const fm = record.booking ? record : (await this.firstMileRepo.findById(record.bookingId, { relations: { booking: true } }));
|
||||
const fm = record.booking ? record : (await this.firstMileRepo.findById(record.id, { relations: { booking: true } }));
|
||||
if (!fm) return null;
|
||||
if (!fm.booking?.companyId) {
|
||||
this.logger.warn(
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTheme } from '@/lib/theme-store';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@/lib/theme-store";
|
||||
import { useAuthStore } from "@/lib/auth-store";
|
||||
|
||||
function ThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const { isDark, setTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
document.documentElement.classList.toggle("dark", isDark);
|
||||
}, [isDark]);
|
||||
|
||||
return <>{children}</>;
|
||||
@@ -26,14 +26,16 @@ function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
}));
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
Reference in New Issue
Block a user