diff --git a/.env.example b/.env.example index c218106b..76bea144 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,9 @@ REQUEST_API_URL="" REQUEST_API_KEY="" NEXT_PUBLIC_REOWN_PROJECT_ID="" WEBHOOK_SECRET="" +CRYPTO_TO_FIAT_PAYEE_ADDRESS="" # Optional # NEXT_PUBLIC_GTM_ID="" +# NEXT_PUBLIC_CRYPTO_TO_FIAT_TRUSTED_ORIGINS="" + diff --git a/drizzle/0005_fine_joystick.sql b/drizzle/0005_fine_joystick.sql new file mode 100644 index 00000000..69983036 --- /dev/null +++ b/drizzle/0005_fine_joystick.sql @@ -0,0 +1,84 @@ +CREATE TYPE "public"."account_type" AS ENUM('checking', 'savings');--> statement-breakpoint +CREATE TYPE "public"."agreement_status" AS ENUM('not_started', 'pending', 'completed');--> statement-breakpoint +CREATE TYPE "public"."beneficiary_type" AS ENUM('individual', 'business');--> statement-breakpoint +CREATE TYPE "public"."gender" AS ENUM('male', 'female', 'other', 'prefer_not_to_say');--> statement-breakpoint +CREATE TYPE "public"."kyc_status" AS ENUM('not_started', 'initiated', 'pending', 'approved');--> statement-breakpoint +CREATE TYPE "public"."payment_details_status" AS ENUM('pending', 'approved', 'rejected');--> statement-breakpoint +CREATE TYPE "public"."rails_type" AS ENUM('local', 'swift', 'wire');--> statement-breakpoint +CREATE TYPE "public"."request_status" AS ENUM('pending', 'paid', 'crypto_paid', 'offramp_initiated', 'offramp_failed', 'offramp_pending', 'processing', 'overdue');--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "easyinvoice_payment_details_payers" ( + "id" text PRIMARY KEY NOT NULL, + "paymentDetailsId" text NOT NULL, + "payerId" text NOT NULL, + "payment_details_status" "payment_details_status" DEFAULT 'pending', + "externalPaymentDetailId" text NOT NULL, + "created_at" timestamp DEFAULT now() +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "easyinvoice_payment_details" ( + "id" text PRIMARY KEY NOT NULL, + "userId" text NOT NULL, + "bankName" text NOT NULL, + "accountName" text NOT NULL, + "accountNumber" text, + "routingNumber" text, + "account_type" "account_type" DEFAULT 'checking', + "sortCode" text, + "iban" text, + "swiftBic" text, + "documentNumber" text, + "documentType" text, + "ribNumber" text, + "bsbNumber" text, + "ncc" text, + "branchCode" text, + "bankCode" text, + "ifsc" text, + "beneficiary_type" "beneficiary_type" NOT NULL, + "dateOfBirth" text, + "addressLine1" text NOT NULL, + "addressLine2" text, + "city" text NOT NULL, + "state" text, + "postalCode" text NOT NULL, + "country" text NOT NULL, + "rails_type" "rails_type" DEFAULT 'local', + "currency" text NOT NULL, + "phone" text, + "businessActivity" text, + "nationality" text, + "gender" "gender", + "created_at" timestamp DEFAULT now(), + "updated_at" timestamp DEFAULT now() +); +--> statement-breakpoint +ALTER TABLE "easyinvoice_request" RENAME COLUMN "status" TO "request_status";--> statement-breakpoint +ALTER TABLE "easyinvoice_request" ADD COLUMN "originalRequestId" text;--> statement-breakpoint +ALTER TABLE "easyinvoice_request" ADD COLUMN "isCryptoToFiatAvailable" boolean DEFAULT false;--> statement-breakpoint +ALTER TABLE "easyinvoice_request" ADD COLUMN "paymentDetailsId" text;--> statement-breakpoint +ALTER TABLE "easyinvoice_user" ADD COLUMN "agreement_status" "agreement_status" DEFAULT 'pending';--> statement-breakpoint +ALTER TABLE "easyinvoice_user" ADD COLUMN "kyc_status" "kyc_status" DEFAULT 'pending';--> statement-breakpoint +ALTER TABLE "easyinvoice_user" ADD COLUMN "isCompliant" boolean DEFAULT false;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "easyinvoice_payment_details_payers" ADD CONSTRAINT "easyinvoice_payment_details_payers_paymentDetailsId_easyinvoice_payment_details_id_fk" FOREIGN KEY ("paymentDetailsId") REFERENCES "public"."easyinvoice_payment_details"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "easyinvoice_payment_details_payers" ADD CONSTRAINT "easyinvoice_payment_details_payers_payerId_easyinvoice_user_id_fk" FOREIGN KEY ("payerId") REFERENCES "public"."easyinvoice_user"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "easyinvoice_payment_details" ADD CONSTRAINT "easyinvoice_payment_details_userId_easyinvoice_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."easyinvoice_user"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "easyinvoice_request" ADD CONSTRAINT "easyinvoice_request_paymentDetailsId_easyinvoice_payment_details_id_fk" FOREIGN KEY ("paymentDetailsId") REFERENCES "public"."easyinvoice_payment_details"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 00000000..60b04283 --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -0,0 +1,733 @@ +{ + "id": "d2b8ba0a-2ee9-479d-94ae-2455fdd6f4c6", + "prevId": "a1445486-e7f1-4c77-b5d9-0956ed8c3eae", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.easyinvoice_invoice_me": { + "name": "easyinvoice_invoice_me", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_invoice_me_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_invoice_me_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_invoice_me", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_payment_details_payers": { + "name": "easyinvoice_payment_details_payers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paymentDetailsId": { + "name": "paymentDetailsId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payerId": { + "name": "payerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_details_status": { + "name": "payment_details_status", + "type": "payment_details_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "externalPaymentDetailId": { + "name": "externalPaymentDetailId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_payment_details_payers_paymentDetailsId_easyinvoice_payment_details_id_fk": { + "name": "easyinvoice_payment_details_payers_paymentDetailsId_easyinvoice_payment_details_id_fk", + "tableFrom": "easyinvoice_payment_details_payers", + "tableTo": "easyinvoice_payment_details", + "columnsFrom": ["paymentDetailsId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "easyinvoice_payment_details_payers_payerId_easyinvoice_user_id_fk": { + "name": "easyinvoice_payment_details_payers_payerId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_payment_details_payers", + "tableTo": "easyinvoice_user", + "columnsFrom": ["payerId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_payment_details": { + "name": "easyinvoice_payment_details", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bankName": { + "name": "bankName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accountName": { + "name": "accountName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accountNumber": { + "name": "accountNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routingNumber": { + "name": "routingNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_type": { + "name": "account_type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'checking'" + }, + "sortCode": { + "name": "sortCode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "iban": { + "name": "iban", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "swiftBic": { + "name": "swiftBic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "documentNumber": { + "name": "documentNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "documentType": { + "name": "documentType", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ribNumber": { + "name": "ribNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bsbNumber": { + "name": "bsbNumber", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ncc": { + "name": "ncc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branchCode": { + "name": "branchCode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bankCode": { + "name": "bankCode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ifsc": { + "name": "ifsc", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "beneficiary_type": { + "name": "beneficiary_type", + "type": "beneficiary_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dateOfBirth": { + "name": "dateOfBirth", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "addressLine1": { + "name": "addressLine1", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "addressLine2": { + "name": "addressLine2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "postalCode": { + "name": "postalCode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rails_type": { + "name": "rails_type", + "type": "rails_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'local'" + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "businessActivity": { + "name": "businessActivity", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "nationality": { + "name": "nationality", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gender": { + "name": "gender", + "type": "gender", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_payment_details_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_payment_details_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_payment_details", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_request": { + "name": "easyinvoice_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dueDate": { + "name": "dueDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuedDate": { + "name": "issuedDate", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clientName": { + "name": "clientName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "clientEmail": { + "name": "clientEmail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creatorName": { + "name": "creatorName", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creatorEmail": { + "name": "creatorEmail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "items": { + "name": "items", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoiceCurrency": { + "name": "invoiceCurrency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paymentCurrency": { + "name": "paymentCurrency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_status": { + "name": "request_status", + "type": "request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "payee": { + "name": "payee", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requestId": { + "name": "requestId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "paymentReference": { + "name": "paymentReference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "originalRequestPaymentReference": { + "name": "originalRequestPaymentReference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "originalRequestId": { + "name": "originalRequestId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoicedTo": { + "name": "invoicedTo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recurrence": { + "name": "recurrence", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "isRecurrenceStopped": { + "name": "isRecurrenceStopped", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "isCryptoToFiatAvailable": { + "name": "isCryptoToFiatAvailable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "paymentDetailsId": { + "name": "paymentDetailsId", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_request_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_request_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_request", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "easyinvoice_request_paymentDetailsId_easyinvoice_payment_details_id_fk": { + "name": "easyinvoice_request_paymentDetailsId_easyinvoice_payment_details_id_fk", + "tableFrom": "easyinvoice_request", + "tableTo": "easyinvoice_payment_details", + "columnsFrom": ["paymentDetailsId"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_session": { + "name": "easyinvoice_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "easyinvoice_session_userId_easyinvoice_user_id_fk": { + "name": "easyinvoice_session_userId_easyinvoice_user_id_fk", + "tableFrom": "easyinvoice_session", + "tableTo": "easyinvoice_user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.easyinvoice_user": { + "name": "easyinvoice_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "googleId": { + "name": "googleId", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agreement_status": { + "name": "agreement_status", + "type": "agreement_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "kyc_status": { + "name": "kyc_status", + "type": "kyc_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "isCompliant": { + "name": "isCompliant", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "easyinvoice_user_googleId_unique": { + "name": "easyinvoice_user_googleId_unique", + "nullsNotDistinct": false, + "columns": ["googleId"] + }, + "easyinvoice_user_email_unique": { + "name": "easyinvoice_user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": ["checking", "savings"] + }, + "public.agreement_status": { + "name": "agreement_status", + "schema": "public", + "values": ["not_started", "pending", "completed"] + }, + "public.beneficiary_type": { + "name": "beneficiary_type", + "schema": "public", + "values": ["individual", "business"] + }, + "public.gender": { + "name": "gender", + "schema": "public", + "values": ["male", "female", "other", "prefer_not_to_say"] + }, + "public.kyc_status": { + "name": "kyc_status", + "schema": "public", + "values": ["not_started", "initiated", "pending", "approved"] + }, + "public.payment_details_status": { + "name": "payment_details_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.rails_type": { + "name": "rails_type", + "schema": "public", + "values": ["local", "swift", "wire"] + }, + "public.request_status": { + "name": "request_status", + "schema": "public", + "values": [ + "pending", + "paid", + "crypto_paid", + "offramp_initiated", + "offramp_failed", + "offramp_pending", + "processing", + "overdue" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 94d322c7..1abda767 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1741182006473, "tag": "0004_thankful_deathstrike", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1747822734112, + "tag": "0005_fine_joystick", + "breakpoints": true } ] } diff --git a/package-lock.json b/package-lock.json index 923c3cf0..2440a5e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,23 @@ { "name": "easy-invoice", - "version": "0.4.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "easy-invoice", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@hookform/resolvers": "^3.9.1", + "@hookform/resolvers": "^3.10.0", "@next/third-parties": "^15.1.7", "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.2", - "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", @@ -32,6 +33,7 @@ "clsx": "^2.1.1", "crypto-js": "^4.2.0", "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "drizzle-kit": "^0.28.1", "drizzle-orm": "^0.36.3", "ethers": "^5.7.2", @@ -42,7 +44,7 @@ "pg": "^8.13.1", "react": "^18", "react-dom": "^18", - "react-hook-form": "^7.53.2", + "react-hook-form": "^7.54.2", "sonner": "^1.7.4", "superjson": "^2.2.1", "tailwind-merge": "^2.5.4", @@ -50,7 +52,7 @@ "ulid": "^2.3.0", "validator": "^13.12.0", "viem": "^2.21.48", - "zod": "^3.23.8" + "zod": "^3.24.2" }, "devDependencies": { "@biomejs/biome": "^1.9.4", @@ -1843,7 +1845,6 @@ "version": "3.10.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", - "license": "MIT", "peerDependencies": { "react-hook-form": "^7.0.0" } @@ -2431,7 +2432,6 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", - "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", @@ -2596,7 +2596,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.2.tgz", "integrity": "sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==", - "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, @@ -2836,7 +2835,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", - "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.1" }, @@ -4366,6 +4364,14 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", @@ -6702,7 +6708,6 @@ "version": "7.54.2", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", - "license": "MIT", "engines": { "node": ">=18.0.0" }, @@ -8116,10 +8121,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", - "license": "MIT", + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 5cd26cd8..494f18f4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "easy-invoice", "description": "A Request Network demo app for easy invoice creation using Request API", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "scripts": { "dev": "chmod +x dev/dev-startup.sh && ./dev/dev-startup.sh ", @@ -18,14 +18,15 @@ "prepare": "husky" }, "dependencies": { - "@hookform/resolvers": "^3.9.1", + "@hookform/resolvers": "^3.10.0", "@next/third-parties": "^15.1.7", "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.2", - "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", @@ -41,6 +42,7 @@ "clsx": "^2.1.1", "crypto-js": "^4.2.0", "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "drizzle-kit": "^0.28.1", "drizzle-orm": "^0.36.3", "ethers": "^5.7.2", @@ -51,7 +53,7 @@ "pg": "^8.13.1", "react": "^18", "react-dom": "^18", - "react-hook-form": "^7.53.2", + "react-hook-form": "^7.54.2", "sonner": "^1.7.4", "superjson": "^2.2.1", "tailwind-merge": "^2.5.4", @@ -59,7 +61,7 @@ "ulid": "^2.3.0", "validator": "^13.12.0", "viem": "^2.21.48", - "zod": "^3.23.8" + "zod": "^3.24.2" }, "devDependencies": { "@biomejs/biome": "^1.9.4", diff --git a/src/app/api/webhook/route.ts b/src/app/api/webhook/route.ts index d5c04ec7..cd6a5ae9 100644 --- a/src/app/api/webhook/route.ts +++ b/src/app/api/webhook/route.ts @@ -1,15 +1,46 @@ import crypto from "node:crypto"; +import { ResourceNotFoundError } from "@/lib/errors"; import { getInvoiceCount } from "@/lib/invoice"; import { generateInvoiceNumber } from "@/lib/invoice/client"; import { db } from "@/server/db"; -import { requestTable } from "@/server/db/schema"; -import { eq } from "drizzle-orm"; +import { + paymentDetailsPayersTable, + type requestStatusEnum, + requestTable, + userTable, +} from "@/server/db/schema"; +import { and, eq, not } from "drizzle-orm"; import { NextResponse } from "next/server"; import { ulid } from "ulid"; +/** + * Updates the request status in the database + */ +async function updateRequestStatus( + requestId: string, + status: (typeof requestStatusEnum.enumValues)[number], +) { + await db.transaction(async (tx) => { + const result = await tx + .update(requestTable) + .set({ status }) + .where(eq(requestTable.requestId, requestId)) + .returning({ id: requestTable.id }); + + if (!result.length) { + throw new ResourceNotFoundError( + `No request found with request ID: ${requestId}`, + ); + } + }); +} + export async function POST(req: Request) { + let webhookData: Record = {}; + try { const body = await req.json(); + webhookData = body; const signature = req.headers.get("x-request-network-signature"); const webhookSecret = process.env.WEBHOOK_SECRET; @@ -27,41 +58,46 @@ export async function POST(req: Request) { return NextResponse.json({ error: "Invalid signature" }, { status: 401 }); } - const { paymentReference, event, originalRequestPaymentReference } = body; + const { + requestId, + event, + originalRequestId, + isCryptoToFiat, + paymentReference, + } = body; switch (event) { case "payment.confirmed": - await db.transaction(async (tx) => { - const result = await tx - .update(requestTable) - .set({ - status: "paid", - }) - .where(eq(requestTable.paymentReference, paymentReference)) - .returning({ id: requestTable.id }); - - if (!result.length) { - throw new Error( - `No request found with payment reference: ${paymentReference}`, - ); - } - }); + await updateRequestStatus( + requestId, + isCryptoToFiat ? "crypto_paid" : "paid", + ); + break; + case "settlement.initiated": + await updateRequestStatus(requestId, "offramp_initiated"); + break; + case "settlement.failed": + case "settlement.bounced": + await updateRequestStatus(requestId, "offramp_failed"); + break; + case "settlement.pending_internal_assessment": + case "settlement.ongoing_checks": + case "settlement.sending_fiat": + await updateRequestStatus(requestId, "offramp_pending"); + break; + case "settlement.fiat_sent": + await updateRequestStatus(requestId, "paid"); break; case "request.recurring": await db.transaction(async (tx) => { const originalRequests = await tx .select() .from(requestTable) - .where( - eq( - requestTable.paymentReference, - originalRequestPaymentReference, - ), - ); + .where(eq(requestTable.requestId, originalRequestId)); if (!originalRequests.length) { - throw new Error( - `No original request found with payment reference: ${originalRequestPaymentReference}`, + throw new ResourceNotFoundError( + `No original request found with request ID: ${originalRequestId}`, ); } @@ -103,19 +139,71 @@ export async function POST(req: Request) { invoiceNumber, issuedDate: now.toISOString(), dueDate: newDueDate.toISOString(), + requestId: requestId, + originalRequestId: originalRequestId, paymentReference: paymentReference, - originalRequestPaymentReference: originalRequestPaymentReference, status: "pending", }); }); break; + case "compliance.updated": { + const complianceUpdateResult = await db + .update(userTable) + .set({ + isCompliant: body.isCompliant, + kycStatus: body.kycStatus, + agreementStatus: body.agreementStatus, + }) + .where(eq(userTable.email, body.clientUserId)) + .returning({ id: userTable.id }); + + if (!complianceUpdateResult.length) { + console.warn( + `No user found with email ID: ${body.clientUserId} for compliance update`, + ); + } + break; + } + case "payment_detail.updated": { + const paymentDetailUpdateResult = await db + .update(paymentDetailsPayersTable) + .set({ + status: body.status, + }) + .where( + and( + eq( + paymentDetailsPayersTable.externalPaymentDetailId, + body.paymentDetailsId, + ), + not(eq(paymentDetailsPayersTable.status, "approved")), + ), + ) + .returning({ id: paymentDetailsPayersTable.id }); + + if (!paymentDetailUpdateResult.length) { + console.warn( + `No payment detail found with payment details ID: ${body.paymentDetailsId} for status update`, + ); + } + break; + } default: break; } return NextResponse.json({ success: true }, { status: 200 }); - } catch (error) { - console.error("Payment webhook error:", error); + } catch (error: unknown) { + console.error("Payment webhook error:", { + error, + requestId: webhookData?.requestId, + event: webhookData?.event, + }); + + if (error instanceof ResourceNotFoundError) { + return NextResponse.json({ error: error.message }, { status: 404 }); + } + return NextResponse.json( { error: "Internal server error" }, { status: 500 }, diff --git a/src/app/crypto-to-fiat/page.tsx b/src/app/crypto-to-fiat/page.tsx new file mode 100644 index 00000000..e185a3c7 --- /dev/null +++ b/src/app/crypto-to-fiat/page.tsx @@ -0,0 +1,31 @@ +import { CryptoToFiat } from "@/components/crypto-to-fiat"; +import { Footer } from "@/components/footer"; +import { Header } from "@/components/header"; +import { getCurrentSession } from "@/server/auth"; +import { redirect } from "next/navigation"; + +export default async function CryptoToFiatPage() { + const { user } = await getCurrentSession(); + + // Redirect to home if not logged in + if (!user) { + redirect("/"); + } + + return ( + <> +
+
+

+ Crypto-to-fiat +

+

+ Pay fiat invoices with crypto +

+ + +
+