Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/invoice/invoice-creator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function InvoiceCreator({
}
toast.success("Invoice created successfully");
await utils.invoice.getAll.invalidate();
router.push("/dashboard");
router.push("/invoices");
},
onError: (error) => {
toast.error("Failed to create invoice", {
Expand All @@ -64,7 +64,7 @@ export function InvoiceCreator({
onSuccess: async () => {
toast.success("Invoice created successfully");
await utils.invoice.getAll.invalidate();
router.push("/dashboard");
router.push("/invoices");
},
onError: (error) => {
toast.error("Failed to create invoice", {
Expand Down
22 changes: 2 additions & 20 deletions src/components/invoice/invoice-form/invoice-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -363,20 +363,8 @@ export function InvoiceForm({
async (data: InvoiceFormValues) => {
// Prevent multiple submissions
if (isSubmitting) return;

setIsSubmitting(true);

// If Crypto-to-fiat is enabled but no payment details are linked, show error
if (data.isCryptoToFiatAvailable && !data.paymentDetailsId) {
// Set form error for paymentDetailsId
form.setError("paymentDetailsId", {
type: "required",
message: "Please select a payment method for Crypto-to-fiat payment",
});
setIsSubmitting(false);
return;
}

// Check if payment details have approved status
if (data.isCryptoToFiatAvailable && data.paymentDetailsId) {
const selectedPaymentDetail = linkedPaymentDetails?.find(
Expand Down Expand Up @@ -429,13 +417,7 @@ export function InvoiceForm({
setIsSubmitting(false);
}
},
[
linkedPaymentDetails,
onSubmit,
form.setError,
isSubmitting,
handleBankAccountSuccess,
],
[linkedPaymentDetails, onSubmit, isSubmitting, handleBankAccountSuccess],
);

// Add timeout effect for pending approval modal
Expand Down Expand Up @@ -755,7 +737,7 @@ export function InvoiceForm({
<div />
</div>
{fields.map((field, index) => (
<div key={field.id} className="flex items-end gap-4">
<div key={field.id} className="flex items-start gap-4">
<div className="flex-grow">
<Input
{...form.register(`items.${index}.description`)}
Expand Down
88 changes: 50 additions & 38 deletions src/lib/schemas/invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { INVOICE_CURRENCIES } from "@/lib/constants/currencies";
import { isEthereumAddress } from "validator";
import { z } from "zod";

export const AddressSchema = z
.string()
.refine(isEthereumAddress, "Invalid Ethereum Address");

export const invoiceFormSchema = z
.object({
invoiceNumber: z.string().min(1, "Invoice number is required"),
Expand All @@ -16,7 +20,7 @@ export const invoiceFormSchema = z
z.object({
description: z.string().min(1, "Description is required"),
quantity: z.number().min(1, "Quantity must be at least 1"),
price: z.number().min(0, "Price must be positive"),
price: z.number().positive("Price must be greater than 0"),
}),
)
.min(1, "At least one item is required"),
Expand All @@ -25,51 +29,59 @@ export const invoiceFormSchema = z
required_error: "Please select an invoice currency",
}),
paymentCurrency: z.string().min(1, "Payment currency is required"),
walletAddress: z.string().optional(),
walletAddress: AddressSchema.optional(),
isRecurring: z.boolean().default(false),
startDate: z.string().optional(),
frequency: z.enum(["DAILY", "WEEKLY", "MONTHLY", "YEARLY"]).optional(),
isCryptoToFiatAvailable: z.boolean().default(false),
paymentDetailsId: z.string().optional(),
})
.refine(
(data) => {
// If invoice is recurring, startDate and frequency must be provided
if (data.isRecurring) {
return !!data.startDate && !!data.frequency;
.superRefine((data, ctx) => {
if (data.isRecurring) {
if (!data.startDate) {
ctx.addIssue({
code: "custom",
message: "Start date is required for recurring invoices",
path: ["startDate"],
});
}
return true;
},
{
message: "Start date and frequency are required for recurring invoices",
path: ["isRecurring"],
},
)
.refine(
(data) => {
// Wallet address is required when crypto-to-fiat is not enabled
if (!data.isCryptoToFiatAvailable) {
return !!data.walletAddress && isEthereumAddress(data.walletAddress);

if (!data.frequency) {
ctx.addIssue({
code: "custom",
message: "Frequency is required for recurring invoices",
path: ["frequency"],
});
}
return true;
},
{
message: "Valid wallet address is required for direct crypto payments",
path: ["walletAddress"],
},
)
.refine(
(data) => {
// Payment details are required when crypto-to-fiat is enabled
if (data.isCryptoToFiatAvailable) {
return !!data.paymentDetailsId;
}

if (!data.isCryptoToFiatAvailable) {
if (!data.walletAddress) {
ctx.addIssue({
code: "custom",
message: "Wallet address is required",
path: ["walletAddress"],
});
}
return true;
},
{
message: "Please select a payment method for Crypto-to-fiat payment",
path: ["paymentDetailsId"],
},
);
} else {
if (!data.paymentDetailsId) {
ctx.addIssue({
code: "custom",
message: "Payment details are required for crypto-to-fiat payments",
path: ["paymentDetailsId"],
});
}
}

const dueDate = new Date(data.dueDate).getTime();
const now = new Date().getTime();
if (dueDate < now) {
ctx.addIssue({
code: "custom",
message: "Due date must be in future",
path: ["dueDate"],
});
}
});

export type InvoiceFormValues = z.infer<typeof invoiceFormSchema>;