stripe - strictMode=false to repair inconsistencies
MODIFY
src/integration/billing/BillingIntegration.ts
src/integration/billing/BillingIntegration.ts
-import { BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceDocument, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingStatus, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
+/* eslint-disable @typescript-eslint/member-ordering */
+import { BillingChargeInvoiceAction, BillingDataTransactionStart, BillingDataTransactionStop, BillingDataTransactionUpdate, BillingInvoice, BillingInvoiceDocument, BillingInvoiceItem, BillingInvoiceStatus, BillingOperationResult, BillingTax, BillingUser, BillingUserSynchronizeAction } from '../../types/Billing';
import User, { UserStatus } from '../../types/User';
import BackendError from '../../exception/BackendError';
@@ -58,7 +59,7 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
for (const user of newUsersToSyncInBilling.result) {
// Synchronize user
try {
- await this.synchronizeUser(this.tenantID, user);
+ await this.synchronizeUser(user);
await Logging.logInfo({
tenantID: this.tenantID,
actionOnUser: user,
@@ -103,7 +104,7 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
source: Constants.CENTRAL_SERVER,
action: ServerAction.BILLING_SYNCHRONIZE_USERS,
module: MODULE_NAME, method: 'synchronizeUsers',
- message: `Billing user with ID '${userBillingIDChangedInBilling}' does not exist in e-Mobility`
+ message: `Billing user with ID '${userBillingIDChangedInBilling}' does not exist`
});
continue;
}
@@ -126,7 +127,7 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
}
// Synchronize several times the user in case of fail before setting it in error
try {
- await this.synchronizeUser(this.tenantID, user);
+ await this.synchronizeUser(user);
await Logging.logInfo({
tenantID: this.tenantID,
actionOnUser: user,
@@ -165,34 +166,30 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
return actionsDone;
}
- public async synchronizeUser(tenantID: string, user: User): Promise<void> {
- const exists = await this.userExists(user);
- let newUser: BillingUser;
- if (!exists) {
- newUser = await this.createUser(user);
- } else {
- newUser = await this.updateUser(user);
- }
- await UserStorage.saveUserBillingData(tenantID, user.id, newUser.billingData);
+ public async synchronizeUser(user: User): Promise<BillingUser> {
+ const billingUser = await this._synchronizeUser(user, true /* strictMode */);
+ await Logging.logInfo({
+ tenantID: this.tenantID,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.BILLING_SYNCHRONIZE_USER,
+ actionOnUser: user,
+ module: MODULE_NAME, method: 'synchronizeUser',
+ message: `Successfully synchronized of the user '${user.email}'`,
+ });
+ return billingUser;
}
- public async forceSynchronizeUser(user: User): Promise<void> {
- // let billingUser = await this.getUserByEmail(user.email);
- let billingUser = await this.getUser(user);
- if (billingUser) {
- if (user.billingData) {
- // Only override user's customerID
- user.billingData.customerID = billingUser.billingData.customerID;
- user.billingData.hasSynchroError = false;
- } else {
- billingUser = await this.updateUser(user);
- user.billingData = billingUser.billingData;
- }
- } else {
- user.billingData = (await this.createUser(user)).billingData;
+ public async forceSynchronizeUser(user: User): Promise<BillingUser> {
+ const billingUser = await this._synchronizeUser(user, false /* !strictMode */);
+ if (user?.billingData?.customerID !== billingUser?.billingData?.customerID) {
+ await Logging.logWarning({
+ tenantID: this.tenantID,
+ source: Constants.CENTRAL_SERVER,
+ action: ServerAction.BILLING_SYNCHRONIZE_USER,
+ module: MODULE_NAME, method: 'forceSynchronizeUser',
+ message: `Force Synchronize User - customerID has been repaired - old value ${user?.billingData?.customerID} - ${billingUser?.billingData?.customerID}`
+ });
}
- // Save
- await UserStorage.saveUserBillingData(this.tenantID, user.id, user.billingData);
await Logging.logInfo({
tenantID: this.tenantID,
source: Constants.CENTRAL_SERVER,
@@ -201,6 +198,20 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
module: MODULE_NAME, method: 'forceSynchronizeUser',
message: `Successfully forced the synchronization of the user '${user.email}'`,
});
+ return billingUser;
+ }
+
+ private async _synchronizeUser(user: User, strictMode: boolean): Promise<BillingUser> {
+ // Check whether the customer exists
+ const exists = await this.userExists(user, strictMode); // returns false when the customerID is not set or is inconsistent
+ let billingUser: BillingUser;
+ if (!exists) {
+ billingUser = await this.createUser(user);
+ } else {
+ billingUser = await this.updateUser(user);
+ }
+ await UserStorage.saveUserBillingData(this.tenantID, user.id, billingUser.billingData);
+ return billingUser;
}
public async synchronizeInvoices(user?: User): Promise<BillingUserSynchronizeAction> {
@@ -560,7 +571,7 @@ export default abstract class BillingIntegration<T extends BillingSetting> {
abstract deleteUser(user: User): Promise<void>;
- abstract userExists(user: User): Promise<boolean>;
+ abstract userExists(user: User, strictMode?: boolean): Promise<boolean>;
abstract getTaxes(): Promise<BillingTax[]>;

MODIFY
src/integration/billing/stripe/StripeBillingIntegration.ts
src/integration/billing/stripe/StripeBillingIntegration.ts
@@ -145,15 +145,15 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
return null;
}
- public async userExists(user: User): Promise<boolean> {
+ public async userExists(user: User, strictMode: boolean): Promise<boolean> {
// Check Stripe
await this.checkConnection();
// Make sure the billing data has been provided
if (!user.billingData) {
user = await UserStorage.getUser(this.tenantID, user.id);
}
- // Get customer
- const customer = await this.getStripeCustomer(user);
+ // Retrieve the STRIPE customer (if any)
+ const customer = await this.getStripeCustomer(user, strictMode);
return !!customer;
}
@@ -178,14 +178,6 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
return this.convertToBillingUser(customer);
}
- // public async getUserByEmail(email: string): Promise<BillingUser> {
- // // Check Stripe
- // await this.checkConnection();
- // // Get customer
- // const customer = await this.getCustomerByEmail(email);
- // return this.convertToBillingUser(customer);
- // }
-
public async getTaxes(): Promise<BillingTax[]> {
const taxes = [] as BillingTax[];
let request;
@@ -971,7 +963,7 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
message: 'Cannot create the user'
});
}
- return this.modifyUser(user);
+ return this.generateStripeCustomer(user);
}
public async updateUser(user: User): Promise<BillingUser> {
@@ -986,7 +978,7 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
message: 'Cannot update the user'
});
}
- return this.modifyUser(user);
+ return this.generateStripeCustomer(user);
}
public async deleteUser(user: User): Promise<void> {
@@ -1001,7 +993,7 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
}
}
- private async getStripeCustomer(userOrCustomerID: User | string): Promise<Stripe.Customer> {
+ private async getStripeCustomer(userOrCustomerID: User | string, strictMode = true): Promise<Stripe.Customer> {
await this.checkConnection();
// Get customer
@@ -1022,6 +1014,7 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
// The customerID refers to something which does not exists anymore in the STRIPE account
// May happen when billing settings are changed to point to a different STRIPE account
// ---------------------------------------------------------------------------------------
+ if (strictMode) {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
module: MODULE_NAME, method: 'getStripeCustomer',
@@ -1029,6 +1022,12 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
message: `Stripe Inconsistency: ${error.message as string}`,
detailedMessages: { error: error.message, stack: error.stack }
});
+ } else {
+ // ---------------------------------------------------------------------------------------
+ // Here we do not throw an exception and simply return null!
+ // This to let the caller the chance to repair the inconsistency
+ // ---------------------------------------------------------------------------------------
+ }
}
}
@@ -1036,14 +1035,14 @@ export default class StripeBillingIntegration extends BillingIntegration<StripeB
return null;
}
- private async modifyUser(user: User): Promise<BillingUser> {
+ private async generateStripeCustomer(user: User): Promise<BillingUser> {
await this.checkConnection();
const fullName = Utils.buildUserFullName(user, false, false);
const locale = Utils.getLanguageFromLocale(user.locale).toLocaleLowerCase();
const i18nManager = I18nManager.getInstanceForLocale(user.locale);
const description = i18nManager.translate('billing.generatedUser', { email: user.email });
// Let's check if the STRIPE customer exists
- let customer = await this.getStripeCustomer(user);
+ let customer = await this.getStripeCustomer(user, false /* !strictMode */);
if (!customer) {
customer = await this.stripe.customers.create({
email: user.email,

MODIFY
src/server/rest/v1/service/BillingService.ts
src/server/rest/v1/service/BillingService.ts
@@ -56,7 +56,7 @@ export default class BillingService {
res.json(Object.assign({ connectionIsValid: true }, Constants.REST_RESPONSE_SUCCESS));
} catch (error) {
// Ko
- Logging.logError({
+ await Logging.logError({
tenantID: req.user.tenantID,
user: req.user,
module: MODULE_NAME, method: 'handleCheckBillingConnection',
@@ -154,7 +154,7 @@ export default class BillingService {
if (billingLock) {
try {
// Sync user
- await billingImpl.synchronizeUser(req.user.tenantID, userToSynchronize);
+ await billingImpl.synchronizeUser(userToSynchronize);
} finally {
// Release the lock
await LockingManager.release(billingLock);
@@ -203,10 +203,9 @@ export default class BillingService {
UtilsService.assertObjectExists(action, user, `User '${filteredRequest.id}' does not exist`,
MODULE_NAME, 'handleSynchronizeUser', req.user);
// Get the User lock
- let billingLock = await LockingHelper.createBillingSyncUsersLock(req.user.tenantID);
+ const billingLock = await LockingHelper.createBillingSyncUsersLock(req.user.tenantID);
if (billingLock) {
try {
- // Sync user
await billingImpl.forceSynchronizeUser(user);
} finally {
await LockingManager.release(billingLock);
@@ -221,25 +220,6 @@ export default class BillingService {
user: req.user
});
}
- // Get the Invoice lock
- billingLock = await LockingHelper.createBillingSyncInvoicesLock(req.user.tenantID);
- if (billingLock) {
- try {
- // Sync invoices
- await billingImpl.synchronizeInvoices(user);
- } finally {
- await LockingManager.release(billingLock);
- }
- } else {
- throw new AppError({
- source: Constants.CENTRAL_SERVER,
- errorCode: HTTPError.GENERAL_ERROR,
- message: 'Cannot acquire lock',
- module: MODULE_NAME, method: 'handleSynchronizeUser',
- action: action,
- user: req.user
- });
- }
// Ok
res.json(Constants.REST_RESPONSE_SUCCESS);
next();

MODIFY
test/api/context/BillingContext.ts
test/api/context/BillingContext.ts
@@ -6,7 +6,6 @@ import Cypher from '../../../src/utils/Cypher';
import SettingStorage from '../../../src/storage/mongodb/SettingStorage';
import { StripeBillingSetting } from '../../../src/types/Setting';
import TenantContext from './TenantContext';
-import Transaction from '../../types/Transaction';
import User from '../../../src/types/User';
import config from '../../config';
@@ -40,7 +39,8 @@ export default class BillingContext {
return;
}
await this.saveBillingSettings(BillingContext.getBillingSettings());
- const billingImpl = await BillingFactory.getBillingImpl(this.tenantContext.getTenant().id);
+ const tenantID = this.tenantContext.getTenant().id;
+ const billingImpl = await BillingFactory.getBillingImpl(tenantID);
if (!billingImpl) {
throw new BackendError({
source: Constants.CENTRAL_SERVER,
@@ -53,8 +53,8 @@ export default class BillingContext {
const adminUser: User = this.tenantContext.getUserContext(ContextDefinition.USER_CONTEXTS.DEFAULT_ADMIN);
const basicUser: User = this.tenantContext.getUserContext(ContextDefinition.USER_CONTEXTS.BASIC_USER);
// Synchronize at least these 2 users - this creates a customer on the STRIPE side
- await billingImpl.synchronizeUser(this.tenantContext.getTenant().id, adminUser);
- await billingImpl.synchronizeUser(this.tenantContext.getTenant().id, basicUser);
+ await billingImpl.synchronizeUser(adminUser);
+ await billingImpl.synchronizeUser(basicUser);
}
private async saveBillingSettings(stripeSettings) {

stripe - strictMode=false to repair inconsistencies