periodic billing - process invoices in the creation date order + pagination
MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
@@ -179,19 +179,24 @@ export default abstract class BillingIntegration {
inSuccess: 0,
inError: 0
};
+ // Check connection
await this.checkConnection();
-
- let invoices: DataResult<BillingInvoice>;
- if (this.settings.billing?.periodicBillingAllowed) {
- // Fetch DRAFT and OPEN invoices
- invoices = await BillingStorage.getInvoicesToProcess(this.tenantID);
- } else {
- // Fetch OPEN invoices only
- invoices = await BillingStorage.getInvoicesToPay(this.tenantID);
- }
- // Let's now finalize all invoices and attempt to get it paid
+ // Prepare filtering and sorting
+ const { filter, sort, limit } = this.preparePeriodicBillingQueryParameters(forceOperation);
+ let skip = 0;
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const invoices = await BillingStorage.getInvoices(this.tenantID, filter, { sort, limit, skip });
+ if (Utils.isEmptyArray(invoices.result)) {
+ break;
+ }
+ skip += limit;
for (const invoice of invoices.result) {
try {
+ // Skip invoices that are already PAID or not relevant for the current billing process
+ if (this.invoiceMustBeSkipped(invoice)) {
+ continue;
+ }
// Make sure to avoid trying to charge it again too soon
if (!forceOperation && moment(invoice.createdOn).isSame(moment(), 'day')) {
actionsDone.inSuccess++;
@@ -228,6 +233,7 @@ export default abstract class BillingIntegration {
});
}
}
+ }
return actionsDone;
}
@@ -391,7 +397,7 @@ export default abstract class BillingIntegration {
}
private async _clearAllInvoiceTestData(): Promise<void> {
- const invoices: DataResult<BillingInvoice> = await BillingStorage.getInvoicesInTestMode(this.tenantID);
+ const invoices: DataResult<BillingInvoice> = await BillingStorage.getInvoices(this.tenantID, { liveMode: false }, Constants.DB_PARAMS_MAX_LIMIT);
// Let's now finalize all invoices and attempt to get it paid
for (const invoice of invoices.result) {
try {
@@ -519,6 +525,41 @@ export default abstract class BillingIntegration {
await UserStorage.saveUserBillingData(this.tenantID, user.id, null);
}
+ private invoiceMustBeSkipped(invoice: BillingInvoice): boolean {
+ if (invoice.status === BillingInvoiceStatus.DRAFT && this.settings.billing?.periodicBillingAllowed) {
+ return false;
+ }
+ if (invoice.status === BillingInvoiceStatus.OPEN) {
+ return false;
+ }
+ return true;
+ }
+
+ private preparePeriodicBillingQueryParameters(forceOperation: boolean): { limit: number, sort: Record<string, unknown>, filter: Record<string, unknown> } {
+ // Prepare filtering to process Invoices of the previous month
+ const startDateTime = moment().date(0).date(1).toDate();
+ const endDateTime = moment().date(1).toDate();
+ // Sort by creation date - process the eldest first!
+ const sort = { createdOn: 1 };
+ if (forceOperation) {
+ // Only used when running tests
+ return {
+ filter: {},
+ limit: 1, // Specific limit to test the pagination
+ sort
+ };
+ }
+ return {
+ // ACHTUNG!!! Make sure not to filter on data which is changed while paginating!!!!
+ filter: {
+ startDateTime,
+ endDateTime
+ },
+ limit: Constants.BATCH_PAGE_SIZE,
+ sort
+ };
+ }
+
abstract checkConnection(): Promise<void>;
abstract checkActivationPrerequisites(): Promise<void>;

MODIFY
src/storage/mongodb/BillingStorage.ts
src/storage/mongodb/BillingStorage.ts
@@ -26,28 +26,6 @@ export default class BillingStorage {
return invoicesMDB.count === 1 ? invoicesMDB.result[0] : null;
}
- public static async getInvoicesInTestMode(tenantID: string): Promise<DataResult<BillingInvoice>> {
- // Returns all invoices that are to be finalized and paid
- const invoicesMDB = await BillingStorage.getInvoices(tenantID, { liveMode: false }, Constants.DB_PARAMS_MAX_LIMIT);
- return invoicesMDB;
- }
-
- public static async getInvoicesToProcess(tenantID: string): Promise<DataResult<BillingInvoice>> {
- // Returns all invoices that are to be finalized and paid
- const invoicesMDB = await BillingStorage.getInvoices(tenantID, {
- invoiceStatus: [BillingInvoiceStatus.DRAFT, BillingInvoiceStatus.OPEN]
- }, Constants.DB_PARAMS_MAX_LIMIT);
- return invoicesMDB;
- }
-
- public static async getInvoicesToPay(tenantID: string): Promise<DataResult<BillingInvoice>> {
- // Returns invoices that are already finalized and are to be paid
- const invoicesMDB = await BillingStorage.getInvoices(tenantID, {
- invoiceStatus: [BillingInvoiceStatus.OPEN]
- }, Constants.DB_PARAMS_MAX_LIMIT);
- return invoicesMDB;
- }
-
public static async getInvoices(tenantID: string,
params: {
invoiceIDs?: string[]; billingInvoiceID?: string; search?: string; userIDs?: string[]; invoiceStatus?: BillingInvoiceStatus[];
@@ -135,7 +113,7 @@ export default class BillingStorage {
aggregation.pop();
// Sort
if (!dbParams.sort) {
- dbParams.sort = { name: 1 };
+ dbParams.sort = { _id: 1 };
}
aggregation.push({
$sort: dbParams.sort

periodic billing - process invoices in the creation date order + pagination