diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs index 7ea3ba9..8388bcf 100644 --- a/backend/eslint.config.mjs +++ b/backend/eslint.config.mjs @@ -43,4 +43,10 @@ export default tseslint.config( }], }, }, + { + files: ['**/*.spec.ts', '**/*.test.ts'], + rules: { + '@typescript-eslint/unbound-method': 'off', + }, + }, ); diff --git a/backend/package-lock.json b/backend/package-lock.json index d083cea..65e3e05 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -11,16 +11,19 @@ "license": "MIT", "dependencies": { "@invoisio/soroban-client": "file:../soroban/client", + "@nestjs/axios": "^4.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.1.1", "@nestjs/typeorm": "^11.0.0", "@prisma/adapter-pg": "^7.4.2", "@prisma/client": "^7.4.2", "@stellar/stellar-sdk": "^14.6.0", + "axios": "^1.13.6", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "cross-env": "^10.1.0", @@ -2272,6 +2275,17 @@ "@tybys/wasm-util": "^0.10.0" } }, + "node_modules/@nestjs/axios": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", + "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "axios": "^1.3.1", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "11.0.16", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz", @@ -2509,6 +2523,19 @@ "@nestjs/core": "^11.0.0" } }, + "node_modules/@nestjs/schedule": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.1.1.tgz", + "integrity": "sha512-kQl1RRgi02GJ0uaUGCrXHCcwISsCsJDciCKe38ykJZgnAeeoeVWs8luWtBo4AqAAXm4nS5K8RlV0smHUJ4+2FA==", + "license": "MIT", + "dependencies": { + "cron": "4.4.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0" + } + }, "node_modules/@nestjs/schematics": { "version": "11.0.9", "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", @@ -3355,6 +3382,12 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "license": "MIT" + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -5708,6 +5741,23 @@ "devOptional": true, "license": "MIT" }, + "node_modules/cron": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz", + "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==", + "license": "MIT", + "dependencies": { + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, + "engines": { + "node": ">=18.x" + }, + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/intcreator" + } + }, "node_modules/cross-env": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", @@ -9212,6 +9262,15 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", diff --git a/backend/package.json b/backend/package.json index cd50bb7..b0781d6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -32,16 +32,19 @@ }, "dependencies": { "@invoisio/soroban-client": "file:../soroban/client", + "@nestjs/axios": "^4.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/jwt": "^11.0.2", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", + "@nestjs/schedule": "^6.1.1", "@nestjs/typeorm": "^11.0.0", "@prisma/adapter-pg": "^7.4.2", "@prisma/client": "^7.4.2", "@stellar/stellar-sdk": "^14.6.0", + "axios": "^1.13.6", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "cross-env": "^10.1.0", @@ -115,4 +118,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} diff --git a/backend/prisma/migrations/20260307171500_add_webhooks/migration.sql b/backend/prisma/migrations/20260307171500_add_webhooks/migration.sql new file mode 100644 index 0000000..b5eab13 --- /dev/null +++ b/backend/prisma/migrations/20260307171500_add_webhooks/migration.sql @@ -0,0 +1,29 @@ +-- CreateEnum +CREATE TYPE "DeliveryStatus" AS ENUM ('pending', 'success', 'failed'); + +-- AlterTable +ALTER TABLE "users" ADD COLUMN "webhook_url" TEXT, +ADD COLUMN "webhook_secret" TEXT; + +-- CreateTable +CREATE TABLE "webhook_deliveries" ( + "id" TEXT NOT NULL, + "invoice_id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "url" TEXT NOT NULL, + "payload" JSONB NOT NULL, + "status" "DeliveryStatus" NOT NULL DEFAULT 'pending', + "attempts" INTEGER NOT NULL DEFAULT 0, + "last_attempt_at" TIMESTAMP(3), + "next_attempt_at" TIMESTAMP(3), + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "webhook_deliveries_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "webhook_deliveries" ADD CONSTRAINT "webhook_deliveries_invoice_id_fkey" FOREIGN KEY ("invoice_id") REFERENCES "invoices"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "webhook_deliveries" ADD CONSTRAINT "webhook_deliveries_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 88a9771..0d3da91 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -20,11 +20,39 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + webhookUrl String? @map("webhook_url") + webhookSecret String? @map("webhook_secret") + invoices Invoice[] + webhooks WebhookDelivery[] @@map("users") } +model WebhookDelivery { + id String @id @default(uuid()) + invoiceId String @map("invoice_id") + invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade) + userId String @map("user_id") + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + url String + payload Json + status DeliveryStatus @default(pending) + attempts Int @default(0) + lastAttemptAt DateTime? @map("last_attempt_at") + nextAttemptAt DateTime? @map("next_attempt_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("webhook_deliveries") +} + +enum DeliveryStatus { + pending + success + failed +} + // Invoice model matching backend/src/invoices/entities/invoice.entity.ts model Invoice { id String @id @default(uuid()) @@ -49,6 +77,8 @@ model Invoice { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") + webhooks WebhookDelivery[] + @@map("invoices") } diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 379a2e0..2ca86b0 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -14,6 +14,8 @@ import { HorizonWatcherModule } from "./stellar/horizon-watcher.module"; import { AuthModule } from "./auth/auth.module"; import { UsersModule } from "./users/user.module"; import { PrismaModule } from "./prisma/prisma.module"; +import { ScheduleModule } from "@nestjs/schedule"; +import { WebhooksModule } from "./webhooks/webhooks.module"; /** * Root application module @@ -51,12 +53,14 @@ import { PrismaModule } from "./prisma/prisma.module"; }), }), PrismaModule, + ScheduleModule.forRoot(), HealthModule, InvoicesModule, StellarModule, HorizonWatcherModule, AuthModule, UsersModule, + WebhooksModule, ], }) export class AppModule {} diff --git a/backend/src/invoices/invoices.module.ts b/backend/src/invoices/invoices.module.ts index 0bbf962..fb6e8e5 100644 --- a/backend/src/invoices/invoices.module.ts +++ b/backend/src/invoices/invoices.module.ts @@ -7,6 +7,7 @@ import { StellarModule } from "../stellar/stellar.module"; import { SorobanModule } from "../soroban/soroban.module"; import { AuthGuard } from "../auth/auth.guard"; import { PrismaModule } from "../prisma/prisma.module"; +import { WebhooksModule } from "../webhooks/webhooks.module"; /** * Invoices module @@ -17,6 +18,7 @@ import { PrismaModule } from "../prisma/prisma.module"; StellarModule, SorobanModule, PrismaModule, + WebhooksModule, JwtModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], diff --git a/backend/src/invoices/invoices.service.spec.ts b/backend/src/invoices/invoices.service.spec.ts index 7e99b3b..f262798 100644 --- a/backend/src/invoices/invoices.service.spec.ts +++ b/backend/src/invoices/invoices.service.spec.ts @@ -7,6 +7,7 @@ import { CreateInvoiceDto } from "./dto/create-invoice.dto"; import { validate } from "class-validator"; import { plainToInstance } from "class-transformer"; import { PrismaService } from "../prisma/prisma.service"; +import { WebhooksService } from "../webhooks/webhooks.service"; describe("InvoicesService", () => { let service: InvoicesService; @@ -195,6 +196,10 @@ describe("InvoicesService", () => { }; }; + const mockWebhooksService = { + enqueueWebhook: jest.fn().mockResolvedValue(undefined), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -203,6 +208,7 @@ describe("InvoicesService", () => { { provide: StellarService, useValue: mockStellarService }, { provide: SorobanService, useValue: mockSorobanService }, { provide: PrismaService, useFactory: mockPrisma }, + { provide: WebhooksService, useValue: mockWebhooksService }, ], }).compile(); diff --git a/backend/src/invoices/invoices.service.ts b/backend/src/invoices/invoices.service.ts index 1807a98..f447024 100644 --- a/backend/src/invoices/invoices.service.ts +++ b/backend/src/invoices/invoices.service.ts @@ -11,6 +11,7 @@ import { StellarService } from "../stellar/stellar.service"; import { SorobanService } from "../soroban/soroban.service"; import { PrismaService } from "../prisma/prisma.service"; import { Prisma, InvoiceStatus } from "@prisma/client"; +import { WebhooksService } from "../webhooks/webhooks.service"; /** * Invoices service — manages invoice lifecycle and Soroban on-chain settlement. @@ -24,6 +25,7 @@ export class InvoicesService implements OnModuleInit { private readonly stellarService: StellarService, private readonly sorobanService: SorobanService, private readonly prisma: PrismaService, + private readonly webhooksService: WebhooksService, ) {} async onModuleInit() { @@ -122,6 +124,10 @@ export class InvoicesService implements OnModuleInit { where: { id }, data: { status }, }); + + // Enqueue webhook + await this.webhooksService.enqueueWebhook(id, status, updated.txHash); + return this.normalizeInvoice(updated); } @@ -136,6 +142,10 @@ export class InvoicesService implements OnModuleInit { where: { id }, data: { status: "paid", txHash: txHash }, }); + + // Enqueue webhook + await this.webhooksService.enqueueWebhook(id, "paid", txHash); + return this.normalizeInvoice(updated); } diff --git a/backend/src/stellar/soroban.integration.spec.ts b/backend/src/stellar/soroban.integration.spec.ts index 4c0e392..20fbb26 100644 --- a/backend/src/stellar/soroban.integration.spec.ts +++ b/backend/src/stellar/soroban.integration.spec.ts @@ -107,13 +107,11 @@ describe("Soroban Integration", () => { await new Promise((resolve) => setTimeout(resolve, 100)); - // eslint-disable-next-line @typescript-eslint/unbound-method expect(invoicesService.markAsPaid).toHaveBeenCalledWith( mockInvoice.id, "horizon-tx-hash-abc", ); - // eslint-disable-next-line @typescript-eslint/unbound-method expect(sorobanService.recordPayment).toHaveBeenCalledWith({ invoiceId: mockInvoice.memo, payer: mockPaymentRecord.from, @@ -122,7 +120,6 @@ describe("Soroban Integration", () => { amount: "100000000", }); - // eslint-disable-next-line @typescript-eslint/unbound-method expect(invoicesService.updateSorobanMetadata).toHaveBeenCalledWith( mockInvoice.id, "soroban-tx-hash-xyz", @@ -146,11 +143,10 @@ describe("Soroban Integration", () => { await new Promise((resolve) => setTimeout(resolve, 100)); - // eslint-disable-next-line @typescript-eslint/unbound-method expect(invoicesService.markAsPaid).toHaveBeenCalled(); - // eslint-disable-next-line @typescript-eslint/unbound-method + expect(sorobanService.recordPayment).toHaveBeenCalled(); - // eslint-disable-next-line @typescript-eslint/unbound-method + expect(invoicesService.updateSorobanMetadata).not.toHaveBeenCalled(); }); @@ -179,7 +175,6 @@ describe("Soroban Integration", () => { await new Promise((resolve) => setTimeout(resolve, 100)); - // eslint-disable-next-line @typescript-eslint/unbound-method expect(sorobanService.recordPayment).toHaveBeenCalledWith( expect.objectContaining({ assetCode: "USDC", diff --git a/backend/src/webhooks/webhooks.module.ts b/backend/src/webhooks/webhooks.module.ts new file mode 100644 index 0000000..bc8201e --- /dev/null +++ b/backend/src/webhooks/webhooks.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { PrismaModule } from "../prisma/prisma.module"; +import { WebhooksService } from "./webhooks.service"; + +@Module({ + imports: [PrismaModule], + providers: [WebhooksService], + exports: [WebhooksService], +}) +export class WebhooksModule {} diff --git a/backend/src/webhooks/webhooks.service.spec.ts b/backend/src/webhooks/webhooks.service.spec.ts new file mode 100644 index 0000000..db93265 --- /dev/null +++ b/backend/src/webhooks/webhooks.service.spec.ts @@ -0,0 +1,152 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { WebhooksService } from "./webhooks.service"; +import { PrismaService } from "../prisma/prisma.service"; +import axios from "axios"; + +jest.mock("axios"); +const mockedAxios = axios as jest.Mocked; + +describe("WebhooksService", () => { + let service: WebhooksService; + let prisma: PrismaService; + + const mockPrismaService = { + invoice: { + findUnique: jest.fn(), + }, + webhookDelivery: { + create: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + }, + }; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + WebhooksService, + { provide: PrismaService, useValue: mockPrismaService }, + ], + }).compile(); + + service = module.get(WebhooksService); + prisma = module.get(PrismaService); + }); + + describe("enqueueWebhook", () => { + it("should enqueue a delivery if user has a webhook URL configured", async () => { + mockPrismaService.invoice.findUnique.mockResolvedValue({ + id: "inv-1", + userId: "user-1", + user: { webhookUrl: "https://example.com/webhook" }, + } as any); + + await service.enqueueWebhook("inv-1", "paid", "hash-123"); + + expect(mockPrismaService.invoice.findUnique).toHaveBeenCalledWith({ + where: { id: "inv-1" }, + include: { user: true }, + }); + expect(mockPrismaService.webhookDelivery.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + invoiceId: "inv-1", + userId: "user-1", + url: "https://example.com/webhook", + status: "pending", + attempts: 0, + }), + }), + ); + }); + + it("should skip enqueueing if no webhook URL is configured", async () => { + mockPrismaService.invoice.findUnique.mockResolvedValue({ + id: "inv-2", + userId: "user-2", + user: { webhookUrl: null }, // no webhook URL + } as any); + + await service.enqueueWebhook("inv-2", "paid", "hash-123"); + expect(mockPrismaService.webhookDelivery.create).not.toHaveBeenCalled(); + }); + }); + + describe("deliver", () => { + it("should execute delivery successfully and update status", async () => { + mockedAxios.post.mockResolvedValue({ status: 200 } as any); + + const delivery = { + id: "del-1", + url: "https://example.com/webhook", + payload: { status: "paid" }, + attempts: 0, + user: { webhookSecret: "secret" }, + }; + + await service.deliver(delivery); + + expect(mockedAxios.post).toHaveBeenCalledTimes(1); + const postArgs = mockedAxios.post.mock.calls[0]; + expect(postArgs[0]).toBe("https://example.com/webhook"); + expect(postArgs[2]?.headers?.["x-invoisio-signature"]).toBeDefined(); // HMAC generated + expect(postArgs[2]?.headers?.["x-idempotency-key"]).toBe("del-1-0"); + + expect(mockPrismaService.webhookDelivery.update).toHaveBeenCalledWith({ + where: { id: "del-1" }, + data: expect.objectContaining({ + status: "success", + attempts: 1, + }), + }); + }); + + it("should apply exponential backoff on failure", async () => { + mockedAxios.post.mockRejectedValue(new Error("Timeout")); + + const delivery = { + id: "del-2", + url: "https://example.com/webhook", + payload: { status: "paid" }, + attempts: 1, // 1 past attempt + user: {}, + }; + + await service.deliver(delivery); + + expect(mockedAxios.post).toHaveBeenCalled(); + expect(mockPrismaService.webhookDelivery.update).toHaveBeenCalledTimes(1); + + const updateCall = + mockPrismaService.webhookDelivery.update.mock.calls[0][0]; + expect(updateCall.where.id).toBe("del-2"); + expect(updateCall.data.attempts).toBe(2); + expect(updateCall.data.status).toBeUndefined(); // Still 'pending' since not 5 retries + expect(updateCall.data.nextAttemptAt).toBeInstanceOf(Date); + }); + + it("should mark as failed permanently after 5 attempts", async () => { + mockedAxios.post.mockRejectedValue(new Error("Network Error")); + + const delivery = { + id: "del-max", + url: "https://example.com/webhook", + payload: { status: "paid" }, + attempts: 4, // meaning this attempt is the 5th + user: {}, + }; + + await service.deliver(delivery); + + expect(mockPrismaService.webhookDelivery.update).toHaveBeenCalledWith({ + where: { id: "del-max" }, + data: expect.objectContaining({ + status: "failed", + attempts: 5, + }), + }); + }); + }); +}); diff --git a/backend/src/webhooks/webhooks.service.ts b/backend/src/webhooks/webhooks.service.ts new file mode 100644 index 0000000..dce2ec8 --- /dev/null +++ b/backend/src/webhooks/webhooks.service.ts @@ -0,0 +1,175 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { Cron, CronExpression } from "@nestjs/schedule"; +import { PrismaService } from "../prisma/prisma.service"; +import axios from "axios"; +import * as crypto from "crypto"; + +@Injectable() +export class WebhooksService { + private readonly logger = new Logger(WebhooksService.name); + private isProcessing = false; + + constructor(private readonly prisma: PrismaService) {} + + /** + * Enqueues a webhook delivery for an invoice status change if the user has a webhook URL configured. + */ + async enqueueWebhook( + invoiceId: string, + status: string, + txHash: string | null, + ): Promise { + const invoice = await this.prisma.invoice.findUnique({ + where: { id: invoiceId }, + include: { user: true }, + }); + + if (!invoice || !invoice.user || !invoice.user.webhookUrl) { + if (!invoice?.user?.webhookUrl) { + this.logger.debug( + `Skipping webhook for invoice ${invoiceId}: No webhook URL configured for user.`, + ); + } + return; + } + + const payload = { + invoiceId: invoice.id, + status, + txHash, + timestamp: new Date().toISOString(), + }; + + await this.prisma.webhookDelivery.create({ + data: { + invoiceId: invoice.id, + userId: invoice.userId!, + url: invoice.user.webhookUrl, + payload: payload, + status: "pending", + attempts: 0, + nextAttemptAt: new Date(), // try immediately or at the next cron tick + }, + }); + + this.logger.log( + `Webhook enqueued for invoice ${invoiceId} and status ${status}.`, + ); + } + + /** + * Process the webhook queue periodically. + * Runs every minute. + */ + @Cron(CronExpression.EVERY_MINUTE) + async processQueue() { + if (this.isProcessing) { + return; + } + + this.isProcessing = true; + try { + const pendingDeliveries = await this.prisma.webhookDelivery.findMany({ + where: { + status: "pending", + nextAttemptAt: { lte: new Date() }, + }, + take: 50, + include: { user: true }, + orderBy: { createdAt: "asc" }, + }); + + if (pendingDeliveries.length > 0) { + this.logger.log( + `Processing ${pendingDeliveries.length} pending webhook deliveries...`, + ); + } + + for (const delivery of pendingDeliveries) { + await this.deliver(delivery); + } + } catch (error) { + this.logger.error("Error processing webhook queue", error); + } finally { + this.isProcessing = false; + } + } + + /** + * Delivers a single webhook payload, handling retries and HMAC signatures. + */ + public async deliver(delivery: any): Promise { + const user = delivery.user; + const secret = user?.webhookSecret; + const payloadStr = JSON.stringify(delivery.payload); + + let signature = ""; + if (secret) { + signature = crypto + .createHmac("sha256", secret) + .update(payloadStr) + .digest("hex"); + } + + // Adding idempotency key based on delivery ID and attempts + const idempotencyKey = `${delivery.id}-${delivery.attempts}`; + + try { + await axios.post(delivery.url, delivery.payload, { + headers: { + "Content-Type": "application/json", + "x-invoisio-signature": signature, + "x-idempotency-key": idempotencyKey, + }, + timeout: 5000, + }); + + // Update on success + await this.prisma.webhookDelivery.update({ + where: { id: delivery.id }, + data: { + status: "success", + lastAttemptAt: new Date(), + attempts: delivery.attempts + 1, + }, + }); + + this.logger.log(`Webhook delivery ${delivery.id} succeeded.`); + } catch (error: any) { + const attempts = delivery.attempts + 1; + this.logger.warn( + `Webhook delivery ${delivery.id} failed (attempt ${attempts}): ${error.message}`, + ); + + if (attempts >= 5) { + await this.prisma.webhookDelivery.update({ + where: { id: delivery.id }, + data: { + status: "failed", + lastAttemptAt: new Date(), + attempts, + }, + }); + this.logger.error( + `Webhook delivery ${delivery.id} permanently failed after 5 attempts.`, + ); + } else { + // Exponential backoff: Math.pow(2, attempts) minutes + // E.g., attempts: 1 -> 2m, 2 -> 4m, 3 -> 8m, 4 -> 16m + const nextAttempt = new Date(); + nextAttempt.setMinutes( + nextAttempt.getMinutes() + Math.pow(2, attempts), + ); + + await this.prisma.webhookDelivery.update({ + where: { id: delivery.id }, + data: { + attempts, + lastAttemptAt: new Date(), + nextAttemptAt: nextAttempt, + }, + }); + } + } + } +} diff --git a/backend/testout.txt b/backend/testout.txt new file mode 100644 index 0000000..1dcf4c6 --- /dev/null +++ b/backend/testout.txt @@ -0,0 +1,654 @@ + +> invoisio-backend@0.0.1 pretest +> npx prisma generate + +Loaded Prisma config from prisma.config.ts. + +Prisma schema loaded from prisma/schema.prisma. + +✔ Generated Prisma Client (v7.4.2) to ./node_modules/@prisma/client in 144ms + +Start by importing your Prisma Client (See: https://pris.ly/d/importing-client) + + + +> invoisio-backend@0.0.1 test +> npx prisma generate && npx jest + +Loaded Prisma config from prisma.config.ts. + +Prisma schema loaded from prisma/schema.prisma. + +✔ Generated Prisma Client (v7.4.2) to ./node_modules/@prisma/client in 141ms + +Start by importing your Prisma Client (See: https://pris.ly/d/importing-client) + + +PASS src/health/health.controller.spec.ts +[Nest] 149468 - 03/07/2026, 4:27:07 PM  ERROR [WebhooksService] Webhook delivery del-max permanently failed after 5 attempts. +PASS src/webhooks/webhooks.service.spec.ts +PASS src/stellar/soroban.service.spec.ts +PASS src/stellar/soroban.integration.spec.ts +PASS src/auth/auth.service.spec.ts +PASS src/stellar/stellar.service.spec.ts +FAIL src/invoices/invoices.service.spec.ts + ● InvoicesService › DTO validation › should reject invalid invoice DTOs + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › DTO validation › should accept a valid dto and uppercase asset_code via transform + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › should be defined + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › findAll › should return paginated invoices + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › findAll › should return invoices with required fields within items + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › findOne › should return a single invoice by id + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › findOne › should throw NotFoundException for non-existent invoice + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › create › should normalize asset_code casing even when provided lowercase + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › create › should create a new invoice + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › create › should create an XLM invoice without asset_issuer + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › create › should generate a unique memo for each invoice + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › create › should add created invoice to the list + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › updateStatus › should update invoice status + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › findByMemo › should find invoice by memo + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + + ● InvoicesService › findByMemo › should return undefined for non-existent memo + + Nest can't resolve dependencies of the InvoicesService (ConfigService, StellarService, SorobanService, PrismaService, ?). Please make sure that the argument WebhooksService at index [4] is available in the RootTestModule context. + + Potential solutions: + - Is RootTestModule a valid NestJS module? + - If WebhooksService is a provider, is it part of the current RootTestModule? + - If WebhooksService is exported from a separate @Module, is that module imported within RootTestModule? + @Module({ + imports: [ /* the Module containing WebhooksService */ ] + }) + + For more common dependency resolution issues, see: https://docs.nestjs.com/faq/common-errors + + 197 | + 198 | beforeEach(async () => { + > 199 | const module: TestingModule = await Test.createTestingModule({ + | ^ + 200 | providers: [ + 201 | InvoicesService, + 202 | { provide: ConfigService, useValue: mockConfigService }, + + at TestingInjector.lookupComponentInParentModules (../node_modules/@nestjs/core/injector/injector.js:290:19) + at TestingInjector.resolveComponentWrapper (../node_modules/@nestjs/testing/testing-injector.js:19:45) + at resolveParam (../node_modules/@nestjs/core/injector/injector.js:140:38) + at async Promise.all (index 4) + at TestingInjector.resolveConstructorParams (../node_modules/@nestjs/core/injector/injector.js:169:27) + at TestingInjector.loadInstance (../node_modules/@nestjs/core/injector/injector.js:75:13) + at TestingInjector.loadProvider (../node_modules/@nestjs/core/injector/injector.js:103:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:56:13 + at async Promise.all (index 3) + at TestingInstanceLoader.createInstancesOfProviders (../node_modules/@nestjs/core/injector/instance-loader.js:55:9) + at ../node_modules/@nestjs/core/injector/instance-loader.js:40:13 + at async Promise.all (index 1) + at TestingInstanceLoader.createInstances (../node_modules/@nestjs/core/injector/instance-loader.js:39:9) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/core/injector/instance-loader.js:22:13) + at TestingInstanceLoader.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-instance-loader.js:9:9) + at TestingModuleBuilder.createInstancesOfDependencies (../node_modules/@nestjs/testing/testing-module.builder.js:118:9) + at TestingModuleBuilder.compile (../node_modules/@nestjs/testing/testing-module.builder.js:74:9) + at Object. (invoices/invoices.service.spec.ts:199:35) + +Test Suites: 1 failed, 6 passed, 7 total +Tests: 15 failed, 36 passed, 51 total +Snapshots: 0 total +Time: 6.3 s, estimated 7 s +Ran all test suites. diff --git a/backend/tsout.txt b/backend/tsout.txt new file mode 100644 index 0000000..e69de29