From 428f9ca2a9bb82075fca4d326a6031742b4c4c89 Mon Sep 17 00:00:00 2001 From: Seweryn Kras Date: Tue, 4 Jun 2024 13:29:24 +0200 Subject: [PATCH] feat: payment events --- src/payment/agreement_payment_process.spec.ts | 44 +++---- src/payment/agreement_payment_process.ts | 41 ++----- src/payment/{types.ts => api.ts} | 24 ++++ src/payment/index.ts | 3 +- src/payment/payment.module.ts | 107 ++++++++++++++---- 5 files changed, 139 insertions(+), 80 deletions(-) rename src/payment/{types.ts => api.ts} (53%) diff --git a/src/payment/agreement_payment_process.spec.ts b/src/payment/agreement_payment_process.spec.ts index e9e14b125..15b9bcede 100644 --- a/src/payment/agreement_payment_process.spec.ts +++ b/src/payment/agreement_payment_process.spec.ts @@ -6,23 +6,23 @@ import { Invoice } from "./invoice"; import { DebitNote } from "./debit_note"; import { GolemPaymentError, PaymentErrorCode } from "./error"; import { GolemUserError } from "../shared/error/golem-error"; -import { IPaymentApi } from "./types"; import { Subject } from "rxjs"; import { RejectionReason } from "./rejection"; +import { PaymentModule } from "./payment.module"; const agreementMock = mock(Agreement); const allocationMock = mock(Allocation); const invoiceMock = mock(Invoice); const debitNoteMock = mock(DebitNote); -const mockPaymentApi = imock(); +const mockPaymentModule = imock(); beforeEach(() => { reset(agreementMock); reset(allocationMock); reset(invoiceMock); reset(debitNoteMock); - reset(mockPaymentApi); + reset(mockPaymentModule); const testProviderInfo = { id: "test-provider-id", @@ -32,8 +32,8 @@ beforeEach(() => { when(agreementMock.getProviderInfo()).thenReturn(testProviderInfo); when(invoiceMock.provider).thenReturn(testProviderInfo); - when(mockPaymentApi.receivedInvoices$).thenReturn(new Subject()); - when(mockPaymentApi.receivedDebitNotes$).thenReturn(new Subject()); + when(mockPaymentModule.observeInvoices()).thenReturn(new Subject()); + when(mockPaymentModule.observeDebitNotes()).thenReturn(new Subject()); }); describe("AgreementPaymentProcess", () => { @@ -45,7 +45,7 @@ describe("AgreementPaymentProcess", () => { when(invoiceMock.amount).thenReturn("0.123"); when(invoiceMock.getStatus()).thenReturn("RECEIVED"); - const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentApi), { + const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentModule), { debitNoteFilter: () => true, invoiceFilter: () => true, }); @@ -54,7 +54,7 @@ describe("AgreementPaymentProcess", () => { const success = await process.addInvoice(invoice); expect(success).toEqual(true); - verify(mockPaymentApi.acceptInvoice(invoice, allocation, "0.123")).called(); + verify(mockPaymentModule.acceptInvoice(invoice, allocation, "0.123")).called(); expect(process.isFinished()).toEqual(true); }); @@ -67,7 +67,7 @@ describe("AgreementPaymentProcess", () => { const process = new AgreementPaymentProcess( instance(agreementMock), instance(allocationMock), - instance(mockPaymentApi), + instance(mockPaymentModule), { debitNoteFilter: () => true, invoiceFilter: () => false, @@ -79,7 +79,7 @@ describe("AgreementPaymentProcess", () => { expect(success).toEqual(false); verify( - mockPaymentApi.rejectInvoice( + mockPaymentModule.rejectInvoice( invoice, "Invoice invoice-id for agreement agreement-id rejected by Invoice Filter", ), @@ -94,7 +94,7 @@ describe("AgreementPaymentProcess", () => { when(invoiceMock.getStatus()).thenReturn("ACCEPTED"); const allocation = instance(allocationMock); - const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentApi), { + const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentModule), { debitNoteFilter: () => true, invoiceFilter: () => true, }); @@ -109,7 +109,7 @@ describe("AgreementPaymentProcess", () => { ), ); - verify(mockPaymentApi.acceptInvoice(invoice, allocation, "0.123")).never(); + verify(mockPaymentModule.acceptInvoice(invoice, allocation, "0.123")).never(); expect(process.isFinished()).toEqual(false); }); }); @@ -124,7 +124,7 @@ describe("AgreementPaymentProcess", () => { when(agreementMock.id).thenReturn("agreement-id"); - const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentApi), { + const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentModule), { debitNoteFilter: () => true, invoiceFilter: () => true, }); @@ -155,7 +155,7 @@ describe("AgreementPaymentProcess", () => { const allocation = instance(allocationMock); const agreement = instance(agreementMock); - const process = new AgreementPaymentProcess(agreement, allocation, instance(mockPaymentApi), { + const process = new AgreementPaymentProcess(agreement, allocation, instance(mockPaymentModule), { debitNoteFilter: () => true, invoiceFilter: () => true, }); @@ -184,7 +184,7 @@ describe("AgreementPaymentProcess", () => { const allocation = instance(allocationMock); const agreement = instance(agreementMock); - const process = new AgreementPaymentProcess(agreement, allocation, instance(mockPaymentApi), { + const process = new AgreementPaymentProcess(agreement, allocation, instance(mockPaymentModule), { debitNoteFilter: () => true, invoiceFilter: () => { throw new Error("invoiceFilter error"); @@ -207,7 +207,7 @@ describe("AgreementPaymentProcess", () => { when(debitNoteMock.totalAmountDue).thenReturn("0.123"); - const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentApi), { + const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentModule), { debitNoteFilter: () => true, invoiceFilter: () => true, }); @@ -217,7 +217,7 @@ describe("AgreementPaymentProcess", () => { const success = await process.addDebitNote(debitNote); expect(success).toEqual(true); - verify(mockPaymentApi.acceptDebitNote(debitNote, allocation, "0.123")).called(); + verify(mockPaymentModule.acceptDebitNote(debitNote, allocation, "0.123")).called(); expect(process.isFinished()).toEqual(false); }); @@ -231,7 +231,7 @@ describe("AgreementPaymentProcess", () => { const process = new AgreementPaymentProcess( instance(agreementMock), instance(allocationMock), - instance(mockPaymentApi), + instance(mockPaymentModule), { debitNoteFilter: () => false, invoiceFilter: () => true, @@ -264,7 +264,7 @@ describe("AgreementPaymentProcess", () => { when(debitNoteMock.id).thenReturn("debit-note-id"); when(debitNoteMock.agreementId).thenReturn("agreement-id"); - const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentApi), { + const process = new AgreementPaymentProcess(instance(agreementMock), allocation, instance(mockPaymentModule), { debitNoteFilter: () => true, invoiceFilter: () => true, }); @@ -277,7 +277,7 @@ describe("AgreementPaymentProcess", () => { const debitNoteSuccess = await process.addDebitNote(debitNote); expect(invoiceSuccess).toEqual(true); - verify(mockPaymentApi.acceptInvoice(invoice, allocation, "0.123")).called(); + verify(mockPaymentModule.acceptInvoice(invoice, allocation, "0.123")).called(); expect(debitNoteSuccess).toEqual(false); verify( @@ -298,7 +298,7 @@ describe("AgreementPaymentProcess", () => { const process = new AgreementPaymentProcess( instance(agreementMock), instance(allocationMock), - instance(mockPaymentApi), + instance(mockPaymentModule), { debitNoteFilter: () => true, invoiceFilter: () => true, @@ -312,7 +312,7 @@ describe("AgreementPaymentProcess", () => { const secondSuccess = await process.addDebitNote(debitNote); expect(secondSuccess).toEqual(false); - verify(mockPaymentApi.rejectDebitNote(debitNote, anything())).never(); + verify(mockPaymentModule.rejectDebitNote(debitNote, anything())).never(); expect(process.isFinished()).toEqual(false); }); }); @@ -324,7 +324,7 @@ describe("AgreementPaymentProcess", () => { const process = new AgreementPaymentProcess( instance(agreementMock), instance(allocationMock), - instance(mockPaymentApi), + instance(mockPaymentModule), { debitNoteFilter: () => { throw new Error("debitNoteFilter error"); diff --git a/src/payment/agreement_payment_process.ts b/src/payment/agreement_payment_process.ts index 86f7515af..2afd4d229 100644 --- a/src/payment/agreement_payment_process.ts +++ b/src/payment/agreement_payment_process.ts @@ -7,10 +7,10 @@ import { defaultLogger, Logger } from "../shared/utils"; import AsyncLock from "async-lock"; import { GolemPaymentError, PaymentErrorCode } from "./error"; import { GolemUserError } from "../shared/error/golem-error"; -import { IPaymentApi } from "./types"; import { getMessageFromApiError } from "../shared/utils/apiErrorMessage"; import { Demand } from "../market"; import { filter } from "rxjs"; +import { PaymentModule } from "./payment.module"; export type DebitNoteFilter = ( debitNote: DebitNote, @@ -58,7 +58,7 @@ export class AgreementPaymentProcess { constructor( public readonly agreement: Agreement, public readonly allocation: Allocation, - public readonly paymentApi: IPaymentApi, + public readonly paymentModule: PaymentModule, options?: Partial, logger?: Logger, ) { @@ -68,11 +68,13 @@ export class AgreementPaymentProcess { debitNoteFilter: options?.debitNoteFilter || (() => true), }; - const invoiceSubscription = this.paymentApi.receivedInvoices$ + const invoiceSubscription = this.paymentModule + .observeInvoices() .pipe(filter((invoice) => invoice.agreementId === this.agreement.id)) .subscribe((invoice) => this.addInvoice(invoice)); - const debitNoteSubscription = this.paymentApi.receivedDebitNotes$ + const debitNoteSubscription = this.paymentModule + .observeDebitNotes() .pipe(filter((debitNote) => debitNote.agreementId === this.agreement.id)) .subscribe((debitNote) => this.addDebitNote(debitNote)); @@ -155,22 +157,14 @@ export class AgreementPaymentProcess { } try { - await this.paymentApi.acceptDebitNote(debitNote, this.allocation, debitNote.totalAmountDue); + await this.paymentModule.acceptDebitNote(debitNote, this.allocation, debitNote.totalAmountDue); this.logger.debug(`DebitNote accepted`, { debitNoteId: debitNote.id, agreementId: debitNote.agreementId, }); - // this.events.emit("accepted", { - // id: this.id, - // agreementId: this.agreementId, - // amount: totalAmountAccepted, - // provider: this.provider, - // }); - return true; } catch (error) { const message = getMessageFromApiError(error); - // this.events.emit("paymentFailed", { id: this.id, agreementId: this.agreementId, reason }); throw new GolemPaymentError( `Unable to accept debit note ${debitNote.id}. ${message}`, PaymentErrorCode.DebitNoteAcceptanceFailed, @@ -189,8 +183,7 @@ export class AgreementPaymentProcess { private async rejectDebitNote(debitNote: DebitNote, rejectionReason: RejectionReason, rejectMessage: string) { try { - // FIXME yagna 0.15 still doesn't support invoice rejections - // await this.paymentApi.rejectDebitNote(debitNote, rejectMessage); + await this.paymentModule.rejectDebitNote(debitNote, rejectMessage); this.logger.warn(`DebitNote rejected`, { reason: rejectMessage }); } catch (error) { const message = getMessageFromApiError(error); @@ -201,8 +194,6 @@ export class AgreementPaymentProcess { debitNote.provider, error, ); - } finally { - // this.events.emit("paymentFailed", { id: this.id, agreementId: this.agreementId, reason: rejection.message }); } } @@ -260,25 +251,15 @@ export class AgreementPaymentProcess { } try { - await this.paymentApi.acceptInvoice(invoice, this.allocation, invoice.amount); + await this.paymentModule.acceptInvoice(invoice, this.allocation, invoice.amount); this.logger.info(`Invoice has been accepted`, { invoiceId: invoice.id, agreementId: invoice.agreementId, amount: invoice.amount, provider: this.agreement.getProviderInfo(), }); - - // this.events.emit("invoiceAccepted", { - // invoiceId: invoice.id, - // agreementId: invoice.agreementId, - // amount: totalAmountAccepted, - // provider: invoice.provider, - // }); } catch (error) { const message = getMessageFromApiError(error); - - // this.events.emit("paymentFailed", { invoiceId: invoice.id, agreementId: invoice.agreementId, reason }); - throw new GolemPaymentError( `Unable to accept invoice ${invoice.id} ${message}`, PaymentErrorCode.InvoiceAcceptanceFailed, @@ -297,7 +278,7 @@ export class AgreementPaymentProcess { message: string, ) { try { - await this.paymentApi.rejectInvoice(invoice, message); + await this.paymentModule.rejectInvoice(invoice, message); this.logger.warn(`Invoice rejected`, { reason: message }); } catch (error) { const message = getMessageFromApiError(error); @@ -308,8 +289,6 @@ export class AgreementPaymentProcess { invoice.provider, error, ); - } finally { - // this.events.emit("paymentFailed", { id: this.id, agreementId: this.agreementId, reason: rejection.message }); } } diff --git a/src/payment/types.ts b/src/payment/api.ts similarity index 53% rename from src/payment/types.ts rename to src/payment/api.ts index aee65bcf6..806ceafa9 100644 --- a/src/payment/types.ts +++ b/src/payment/api.ts @@ -3,6 +3,30 @@ import { Invoice } from "./invoice"; import { DebitNote } from "./debit_note"; import { Allocation } from "./allocation"; +export type PaymentEvents = { + allocationCreated: (allocation: Allocation) => void; + errorCreatingAllocation: (error: Error) => void; + + allocationReleased: (allocation: Allocation) => void; + errorReleasingAllocation: (allocation: Allocation, error: Error) => void; + + allocationAmended: (allocation: Allocation) => void; + errorAmendingAllocation: (allocation: Allocation, error: Error) => void; + + invoiceReceived: (invoice: Invoice) => void; + debitNoteReceived: (debitNote: DebitNote) => void; + + invoiceAccepted: (invoice: Invoice) => void; + invoiceRejected: (invoice: Invoice) => void; + errorAcceptingInvoice: (invoice: Invoice, error: Error) => void; + errorRejectingInvoice: (invoice: Invoice, error: Error) => void; + + debitNoteAccepted: (debitNote: DebitNote) => void; + debitNoteRejected: (debitNote: DebitNote) => void; + errorAcceptingDebitNote: (debitNote: DebitNote, error: Error) => void; + errorRejectingDebitNote: (debitNote: DebitNote, error: Error) => void; +}; + export interface IPaymentApi { receivedInvoices$: Subject; receivedDebitNotes$: Subject; diff --git a/src/payment/index.ts b/src/payment/index.ts index 55459bc7c..16e8fd4f3 100644 --- a/src/payment/index.ts +++ b/src/payment/index.ts @@ -6,6 +6,5 @@ export * as PaymentFilters from "./strategy"; export { GolemPaymentError, PaymentErrorCode } from "./error"; export { InvoiceProcessor, InvoiceAcceptResult } from "./InvoiceProcessor"; export * from "./payment.module"; -export { IPaymentApi } from "./types"; -export { CreateAllocationParams } from "./types"; +export * from "./api"; export { InvoiceFilter, DebitNoteFilter } from "./agreement_payment_process"; diff --git a/src/payment/payment.module.ts b/src/payment/payment.module.ts index 3e13952a4..54d9c0448 100644 --- a/src/payment/payment.module.ts +++ b/src/payment/payment.module.ts @@ -1,12 +1,17 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { EventEmitter } from "eventemitter3"; -import { Allocation, DebitNote, Invoice, InvoiceProcessor, IPaymentApi } from "./index"; - +import { + Allocation, + DebitNote, + Invoice, + InvoiceProcessor, + IPaymentApi, + CreateAllocationParams, + PaymentEvents, +} from "./index"; import { defaultLogger, YagnaApi } from "../shared/utils"; import { Observable } from "rxjs"; import { GolemServices } from "../golem-network/golem-network"; import { PayerDetails } from "./PayerDetails"; -import { CreateAllocationParams } from "./types"; import { AgreementPaymentProcess, PaymentProcessOptions } from "./agreement_payment_process"; import { Agreement } from "../market"; import * as EnvUtils from "../shared/utils/env"; @@ -32,10 +37,8 @@ export interface PaymentModuleOptions { token?: "glm" | "tglm"; } -export interface PaymentModuleEvents {} - export interface PaymentModule { - events: EventEmitter; + events: EventEmitter; observeDebitNotes(): Observable; @@ -72,7 +75,7 @@ export interface PaymentModule { const MAINNETS = Object.freeze(["mainnet", "polygon"]); export class PaymentModuleImpl implements PaymentModule { - events: EventEmitter = new EventEmitter(); + events: EventEmitter = new EventEmitter(); private readonly yagnaApi: YagnaApi; @@ -97,6 +100,17 @@ export class PaymentModuleImpl implements PaymentModule { this.logger = deps.logger; this.yagnaApi = deps.yagna; this.paymentApi = deps.paymentApi; + this.startEmittingPaymentEvents(); + } + + private startEmittingPaymentEvents() { + this.paymentApi.receivedInvoices$.subscribe((invoice) => { + this.events.emit("invoiceReceived", invoice); + }); + + this.paymentApi.receivedDebitNotes$.subscribe((debitNote) => { + this.events.emit("debitNoteReceived", debitNote); + }); } private getPaymentPlatform(): string { @@ -122,40 +136,83 @@ export class PaymentModuleImpl implements PaymentModule { this.logger.info("Creating allocation", { params: params, payer }); - return this.paymentApi.createAllocation({ - budget: params.budget, - paymentPlatform: this.getPaymentPlatform(), - expirationSec: params.expirationSec, - }); + try { + const allocation = await this.paymentApi.createAllocation({ + budget: params.budget, + paymentPlatform: this.getPaymentPlatform(), + expirationSec: params.expirationSec, + }); + this.events.emit("allocationCreated", allocation); + return allocation; + } catch (error) { + this.events.emit("errorCreatingAllocation", error); + throw error; + } } - releaseAllocation(allocation: Allocation): Promise { + async releaseAllocation(allocation: Allocation): Promise { this.logger.info("Releasing allocation", { id: allocation.id }); - return this.paymentApi.releaseAllocation(allocation); + try { + await this.paymentApi.releaseAllocation(allocation); + this.events.emit("allocationReleased", allocation); + } catch (error) { + this.events.emit("errorReleasingAllocation", allocation, error); + throw error; + } } + // eslint-disable-next-line @typescript-eslint/no-unused-vars amendAllocation(allocation: Allocation, _newOpts: CreateAllocationParams): Promise { - throw new Error("Method not implemented."); + this.events.emit("errorAmendingAllocation", allocation, new Error("Amending allocation is not supported yet")); + throw new Error("Amending allocation is not supported yet"); } - acceptInvoice(invoice: Invoice, allocation: Allocation, amount: string): Promise { + async acceptInvoice(invoice: Invoice, allocation: Allocation, amount: string): Promise { this.logger.info("Accepting invoice", { id: invoice.id, allocation: allocation.id, amount }); - return this.paymentApi.acceptInvoice(invoice, allocation, amount); + try { + const acceptedInvoice = await this.paymentApi.acceptInvoice(invoice, allocation, amount); + this.events.emit("invoiceAccepted", acceptedInvoice); + return acceptedInvoice; + } catch (error) { + this.events.emit("errorAcceptingInvoice", invoice, error); + throw error; + } } - rejectInvoice(invoice: Invoice, reason: string): Promise { + async rejectInvoice(invoice: Invoice, reason: string): Promise { this.logger.info("Rejecting invoice", { id: invoice.id, reason }); - return this.paymentApi.rejectInvoice(invoice, reason); + try { + const rejectedInvoice = await this.paymentApi.rejectInvoice(invoice, reason); + this.events.emit("invoiceRejected", rejectedInvoice); + return rejectedInvoice; + } catch (error) { + this.events.emit("errorRejectingInvoice", invoice, error); + throw error; + } } - acceptDebitNote(debitNote: DebitNote, allocation: Allocation, amount: string): Promise { + async acceptDebitNote(debitNote: DebitNote, allocation: Allocation, amount: string): Promise { this.logger.info("Accepting debit note", { id: debitNote.id, allocation: allocation.id, amount }); - return this.paymentApi.acceptDebitNote(debitNote, allocation, amount); + try { + const acceptedDebitNote = await this.paymentApi.acceptDebitNote(debitNote, allocation, amount); + this.events.emit("debitNoteAccepted", acceptedDebitNote); + return acceptedDebitNote; + } catch (error) { + this.events.emit("errorAcceptingDebitNote", debitNote, error); + throw error; + } } - rejectDebitNote(debitNote: DebitNote, reason: string): Promise { + async rejectDebitNote(debitNote: DebitNote, reason: string): Promise { this.logger.info("Rejecting debit note", { id: debitNote.id, reason }); - return this.paymentApi.rejectDebitNote(debitNote, reason); + try { + const rejectedDebitNote = await this.paymentApi.rejectDebitNote(debitNote, reason); + this.events.emit("debitNoteRejected", rejectedDebitNote); + return rejectedDebitNote; + } catch (error) { + this.events.emit("errorAcceptingDebitNote", debitNote, error); + throw error; + } } /** @@ -173,7 +230,7 @@ export class PaymentModuleImpl implements PaymentModule { return new AgreementPaymentProcess( agreement, allocation, - this.paymentApi, + this, options, this.logger.child("agreement-payment-process"), );