From 004c267f31ba197f74e6e16760b293bc007af264 Mon Sep 17 00:00:00 2001 From: Tyagi-Sunny Date: Mon, 5 Aug 2024 20:34:36 +0530 Subject: [PATCH] feat(subscription-service): add billing functionality to subscription service add billing functionality to subscription service BREAKING CHANGE: yes gh-34 --- package-lock.json | 179 ++++++----------- .../20240209122448-add-customer-table.js | 59 ++++++ ...20240209122448-add-customer-table-down.sql | 1 + .../20240209122448-add-customer-table-up.sql | 14 ++ services/subscription-service/package.json | 3 +- .../subscription-service/src/component.ts | 42 ++-- .../billing-customer.controller.ts | 167 ++++++++++++++++ .../controllers/billing-invoice.controller.ts | 184 ++++++++++++++++++ .../billing-payment-source.controller.ts | 134 +++++++++++++ .../src/controllers/webhook.controller.ts | 43 ++++ .../src/interceptors/index.ts | 1 + .../webhook-verifier.interceptor.ts | 46 +++++ services/subscription-service/src/keys.ts | 6 +- .../src/models/billing-customer.model.ts | 51 +++++ .../src/models/dto/address-dto.model.ts | 80 ++++++++ .../src/models/dto/charge-dto.model.ts | 26 +++ .../src/models/dto/customer-dto.model.ts | 69 +++++++ .../src/models/dto/index.ts | 5 + .../src/models/dto/invoice-dto.model.ts | 55 ++++++ .../src/models/dto/payment-dto.model.ts | 55 ++++++ .../subscription-service/src/models/index.ts | 1 + .../subscription-service/src/permissions.ts | 12 ++ .../billing-customer.repository.ts | 24 +++ 23 files changed, 1123 insertions(+), 134 deletions(-) create mode 100644 services/subscription-service/migrations/pg/migrations/20240209122448-add-customer-table.js create mode 100644 services/subscription-service/migrations/pg/migrations/sqls/20240209122448-add-customer-table-down.sql create mode 100644 services/subscription-service/migrations/pg/migrations/sqls/20240209122448-add-customer-table-up.sql create mode 100644 services/subscription-service/src/controllers/billing-customer.controller.ts create mode 100644 services/subscription-service/src/controllers/billing-invoice.controller.ts create mode 100644 services/subscription-service/src/controllers/billing-payment-source.controller.ts create mode 100644 services/subscription-service/src/controllers/webhook.controller.ts create mode 100644 services/subscription-service/src/interceptors/index.ts create mode 100644 services/subscription-service/src/interceptors/webhook-verifier.interceptor.ts create mode 100644 services/subscription-service/src/models/billing-customer.model.ts create mode 100644 services/subscription-service/src/models/dto/address-dto.model.ts create mode 100644 services/subscription-service/src/models/dto/charge-dto.model.ts create mode 100644 services/subscription-service/src/models/dto/customer-dto.model.ts create mode 100644 services/subscription-service/src/models/dto/index.ts create mode 100644 services/subscription-service/src/models/dto/invoice-dto.model.ts create mode 100644 services/subscription-service/src/models/dto/payment-dto.model.ts create mode 100644 services/subscription-service/src/repositories/billing-customer.repository.ts diff --git a/package-lock.json b/package-lock.json index 3f7adf1..22c0417 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4957,11 +4957,6 @@ "safe-buffer": "^5.1.1" } }, - "node_modules/bl/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/bl/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -5620,6 +5615,18 @@ "node": "*" } }, + "node_modules/chargebee": { + "version": "2.39.0", + "resolved": "https://registry.npmjs.org/chargebee/-/chargebee-2.39.0.tgz", + "integrity": "sha512-uix5+6z5VolJFv2PKa6VWES1MoDnCuEH0UGHGa3NI/VC/DVpA/NCPA4zbzrUoZ4A/Aa9awiizEbScqorfE/ylA==", + "dependencies": { + "q": ">=1.0.1", + "safer-buffer": "2.1.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -7071,6 +7078,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/deep-equal/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -7617,6 +7629,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-get-iterator/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -8867,12 +8884,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/get-pkg-repo/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "node_modules/get-pkg-repo/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -10463,9 +10474,9 @@ } }, "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isexe": { "version": "2.0.0", @@ -11629,6 +11640,26 @@ "node": ">=8" } }, + "node_modules/local-billing": { + "name": "@local/billing", + "version": "0.0.1", + "resolved": "file:../../billing-package/packages/billing/local-billing-0.0.1.tgz", + "integrity": "sha512-OmhWM5nj5fc/EfsN2oTGHzm0X34gJQND3hfI7dUmsRqvZAoNk4epH2m6XE45iyzbIChyViQ3wJbUDo8swuTvUA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@loopback/rest": "^14.0.0", + "@loopback/rest-explorer": "^7.0.0", + "chargebee": "^2.38.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@loopback/core": "^6.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -13101,12 +13132,6 @@ "msgpack": "bin/msgpack" } }, - "node_modules/msgpack-lite/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "node_modules/msgpack5": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/msgpack5/-/msgpack5-4.5.1.tgz", @@ -13118,11 +13143,6 @@ "safe-buffer": "^5.1.2" } }, - "node_modules/msgpack5/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/msgpack5/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -17018,6 +17038,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -20036,100 +20061,11 @@ "undici-types": "~5.26.4" } }, - "services/orchestrator-service/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "services/orchestrator-service/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "services/orchestrator-service/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "services/orchestrator-service/node_modules/nodemon": { - "version": "3.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "services/orchestrator-service/node_modules/semver": { - "version": "7.6.3", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "services/orchestrator-service/node_modules/simple-update-notifier": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "services/orchestrator-service/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "services/orchestrator-service/node_modules/typescript": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20170,6 +20106,7 @@ "@types/jsonwebtoken": "^9.0.5", "dotenv": "^16.0.3", "dotenv-extended": "^2.9.0", + "local-billing": "file:/home/sunny.tyagi/Desktop/billing-package/packages/billing/local-billing-0.0.1.tgz", "loopback-connector-postgresql": "^7.1.1", "loopback4-authentication": "^12.0.2", "loopback4-authorization": "^7.0.2", @@ -20195,9 +20132,10 @@ } }, "services/subscription-service/node_modules/@types/node": { - "version": "18.19.31", + "version": "18.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.43.tgz", + "integrity": "sha512-Mw/YlgXnyJdEwLoFv2dpuJaDFriX+Pc+0qOBJ57jC1H6cDxIj2xc5yUrdtArDVG0m+KV6622a4p2tenEqB3C/g==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } @@ -20278,9 +20216,10 @@ } }, "services/tenant-management-service/node_modules/@types/node": { - "version": "18.19.31", + "version": "18.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.43.tgz", + "integrity": "sha512-Mw/YlgXnyJdEwLoFv2dpuJaDFriX+Pc+0qOBJ57jC1H6cDxIj2xc5yUrdtArDVG0m+KV6622a4p2tenEqB3C/g==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } diff --git a/services/subscription-service/migrations/pg/migrations/20240209122448-add-customer-table.js b/services/subscription-service/migrations/pg/migrations/20240209122448-add-customer-table.js new file mode 100644 index 0000000..17c1fd9 --- /dev/null +++ b/services/subscription-service/migrations/pg/migrations/20240209122448-add-customer-table.js @@ -0,0 +1,59 @@ +'use strict'; + +let dbm; +let type; +let seed; +let fs = require('fs'); +let path = require('path'); +let Promise; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; + Promise = options.Promise; +}; + +exports.up = function (db) { + let filePath = path.join( + __dirname, + 'sqls', + '20240209122448-add-customer-table-up.sql', + ); + return new Promise(function (resolve, reject) { + fs.readFile(filePath, {encoding: 'utf-8'}, function (err, data) { + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }).then(function (data) { + return db.runSql(data); + }); +}; + +exports.down = function (db) { + let filePath = path.join( + __dirname, + 'sqls', + '20240209122448-add-customer-table-down.sql', + ); + return new Promise(function (resolve, reject) { + fs.readFile(filePath, {encoding: 'utf-8'}, function (err, data) { + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }).then(function (data) { + return db.runSql(data); + }); +}; + +exports._meta = { + version: 1, +}; diff --git a/services/subscription-service/migrations/pg/migrations/sqls/20240209122448-add-customer-table-down.sql b/services/subscription-service/migrations/pg/migrations/sqls/20240209122448-add-customer-table-down.sql new file mode 100644 index 0000000..ea52b6f --- /dev/null +++ b/services/subscription-service/migrations/pg/migrations/sqls/20240209122448-add-customer-table-down.sql @@ -0,0 +1 @@ +drop table main.billing_customer; \ No newline at end of file diff --git a/services/subscription-service/migrations/pg/migrations/sqls/20240209122448-add-customer-table-up.sql b/services/subscription-service/migrations/pg/migrations/sqls/20240209122448-add-customer-table-up.sql new file mode 100644 index 0000000..823ff91 --- /dev/null +++ b/services/subscription-service/migrations/pg/migrations/sqls/20240209122448-add-customer-table-up.sql @@ -0,0 +1,14 @@ +CREATE TABLE main.billing_customer ( + id uuid DEFAULT (md5(((random())::text || (clock_timestamp())::text)))::uuid NOT NULL , tenant_id uuid NOT NULL , + customer_id VARCHAR(255) NOT NULL, + payment_source_id VARCHAR(255), + invoice_id VARCHAR(255), + invoice_status BOOLEAN, + created_on timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL , + modified_on timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL , + deleted boolean DEFAULT false NOT NULL , + deleted_on timestamptz , + deleted_by uuid , + created_by uuid NOT NULL , + modified_by uuid , +); diff --git a/services/subscription-service/package.json b/services/subscription-service/package.json index 4288c54..090959c 100644 --- a/services/subscription-service/package.json +++ b/services/subscription-service/package.json @@ -89,7 +89,8 @@ "loopback4-authorization": "^7.0.2", "swagger-stats": "^0.99.5", "symlink-resolver": "0.2.1", - "tslib": "^2.6.2" + "tslib": "^2.6.2", + "local-billing":"file:/home/sunny.tyagi/Desktop/billing-package/packages/billing/local-billing-0.0.1.tgz" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", diff --git a/services/subscription-service/src/component.ts b/services/subscription-service/src/component.ts index b2902c9..4512f6c 100644 --- a/services/subscription-service/src/component.ts +++ b/services/subscription-service/src/component.ts @@ -22,22 +22,12 @@ import { SECURITY_SCHEME_SPEC, ServiceSequence, } from '@sourceloop/core'; +import {BillingComponent} from 'local-billing'; import {AuthenticationComponent} from 'loopback4-authentication'; import { AuthorizationBindings, AuthorizationComponent, } from 'loopback4-authorization'; -import {SubscriptionServiceBindings} from './keys'; -import {ISubscriptionServiceConfig} from './types'; -import { - BillingCycleRepository, - CurrencyRepository, - PlanItemRepository, - PlanRepository, - ResourceRepository, - ServiceRepository, - SubscriptionRepository, -} from './repositories'; import { BillinCycleController, CurrencyController, @@ -50,11 +40,17 @@ import { ServiceController, SubscriptionController, } from './controllers'; +import {BillingCustomerController} from './controllers/billing-customer.controller'; +import {BillingInvoiceController} from './controllers/billing-invoice.controller'; +import {BillingPaymentSourceController} from './controllers/billing-payment-source.controller'; +import {WebhookController} from './controllers/webhook.controller'; +import {WebhookVerifierProvider} from './interceptors'; +import {SubscriptionServiceBindings, WEBHOOK_VERIFIER} from './keys'; import { BillingCycle, Currency, - PlanItem, Plan, + PlanItem, Resource, Service, Subscription, @@ -63,6 +59,18 @@ import { FeatureToggleBindings, FeatureToggleServiceComponent, } from '@sourceloop/feature-toggle-service'; +import {BillingCustomer} from './models/billing-customer.model'; +import { + BillingCycleRepository, + CurrencyRepository, + PlanItemRepository, + PlanRepository, + ResourceRepository, + ServiceRepository, + SubscriptionRepository, +} from './repositories'; +import {BillingCustomerRepository} from './repositories/billing-customer.repository'; +import {ISubscriptionServiceConfig} from './types'; export class SubscriptionServiceComponent implements Component { constructor( @@ -81,6 +89,7 @@ export class SubscriptionServiceComponent implements Component { .bind(FeatureToggleBindings.Config) .to({bindControllers: true, useCustomSequence: true}); this.application.component(FeatureToggleServiceComponent); + this.application.component(BillingComponent); this.application.api({ openapi: '3.0.0', @@ -108,6 +117,7 @@ export class SubscriptionServiceComponent implements Component { ResourceRepository, ServiceRepository, SubscriptionRepository, + BillingCustomerRepository, ]; this.models = [ @@ -116,9 +126,13 @@ export class SubscriptionServiceComponent implements Component { PlanItem, Plan, Resource, + BillingCustomer, Service, Subscription, ]; + this.bindings = [ + Binding.bind(WEBHOOK_VERIFIER).toProvider(WebhookVerifierProvider), + ]; this.controllers = [ BillinCycleController, @@ -131,6 +145,10 @@ export class SubscriptionServiceComponent implements Component { ServiceController, SubscriptionController, PlanSubscriptionController, + BillingCustomerController, + BillingInvoiceController, + BillingPaymentSourceController, + WebhookController, ]; } diff --git a/services/subscription-service/src/controllers/billing-customer.controller.ts b/services/subscription-service/src/controllers/billing-customer.controller.ts new file mode 100644 index 0000000..e36dc82 --- /dev/null +++ b/services/subscription-service/src/controllers/billing-customer.controller.ts @@ -0,0 +1,167 @@ +import {repository} from '@loopback/repository'; +import { + post, + param, + get, + getModelSchemaRef, + patch, + del, + requestBody, +} from '@loopback/rest'; +import {authorize} from 'loopback4-authorization'; +import {authenticate, STRATEGY} from 'loopback4-authentication'; +import {PermissionKey} from '../permissions'; +import {OPERATION_SECURITY_SPEC, STATUS_CODE} from '@sourceloop/core'; +import {BillingCustomerRepository} from '../repositories/billing-customer.repository'; +import {inject} from '@loopback/core'; +import {BillingComponentBindings, IService} from 'local-billing'; +import {CustomerDto} from '../models/dto/customer-dto.model'; +import {InvoiceDto} from '../models/dto/invoice-dto.model'; +import {PaymentSourceDto} from '../models/dto/payment-dto.model'; +import {BillingCustomer} from '../models/billing-customer.model'; + +const basePath = '/billing-customer'; +export class BillingCustomerController { + constructor( + @repository(BillingCustomerRepository) + public billingCustomerRepository: BillingCustomerRepository, + @inject(BillingComponentBindings.BillingProvider) + private readonly billingProvider: IService< + CustomerDto, + InvoiceDto, + PaymentSourceDto, + CustomerDto, + InvoiceDto, + PaymentSourceDto + >, + ) {} + + @authorize({ + permissions: [PermissionKey.CreateBillingCycle], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @post(basePath, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.OK]: { + description: 'BillingCustomer model instance', + content: { + 'application/json': { + schema: getModelSchemaRef(CustomerDto, { + title: 'NewBillingCustomer', + }), + }, + }, + }, + }, + }) + async create( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(CustomerDto, { + title: 'NewCustomer', + exclude: ['id'], + }), + }, + }, + }) + customerDto: Omit, + @param.header.string('tenantId') tenantId: string, + ): Promise { + const customer = await this.billingProvider.createCustomer(customerDto); + await this.billingCustomerRepository.create( + new BillingCustomer({ + tenantId, + customerId: customer.id, + }), + ); + return customer; + } + + @authorize({ + permissions: [PermissionKey.GetBillingCustomer], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @get(basePath, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.OK]: { + description: 'BillingCustomer model ', + content: {'application/json': {schema: getModelSchemaRef(CustomerDto)}}, + }, + }, + }) + async getCustomer( + @param.header.string('tenantId') tenantId: string, + ): Promise { + const customer = await this.billingCustomerRepository.find({ + where: { + tenantId: tenantId, + }, + }); + if (customer.length == 0) { + throw new Error(' Customer with tenantId is not present'); + } + return this.billingProvider.getCustomers(customer[0].customerId); + } + + @authorize({ + permissions: [PermissionKey.UpdateBillingCustomer], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @patch(`${basePath}/{tenantId}`, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.NO_CONTENT]: { + description: 'BillingCustomer PATCH success', + }, + }, + }) + async updateById( + @param.path.string('tenantId') tenantId: string, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(CustomerDto, {partial: true}), + }, + }, + }) + customerDto: Partial, + ): Promise { + await this.billingProvider.updateCustomerById(tenantId, customerDto); + } + + @authorize({ + permissions: [PermissionKey.DeleteBillingCustomer], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @del(`${basePath}/{tenantId}`, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.NO_CONTENT]: { + description: 'BillingCustomer DELETE success', + }, + }, + }) + async deleteById( + @param.path.string('tenantId') tenantId: string, + ): Promise { + const customer = await this.billingCustomerRepository.find({ + where: {tenantId: tenantId}, + }); + if (customer.length == 0) { + throw new Error(' Customer with tenantId is not present'); + } + await this.billingProvider.deleteCustomer(customer[0].customerId); + await this.billingCustomerRepository.deleteById(customer[0].id); + } +} diff --git a/services/subscription-service/src/controllers/billing-invoice.controller.ts b/services/subscription-service/src/controllers/billing-invoice.controller.ts new file mode 100644 index 0000000..cbfe714 --- /dev/null +++ b/services/subscription-service/src/controllers/billing-invoice.controller.ts @@ -0,0 +1,184 @@ +import {inject} from '@loopback/core'; +import {repository} from '@loopback/repository'; +import { + del, + get, + getModelSchemaRef, + param, + patch, + post, + requestBody, +} from '@loopback/rest'; +import {OPERATION_SECURITY_SPEC, STATUS_CODE} from '@sourceloop/core'; +import {BillingComponentBindings, IService} from 'local-billing'; +import {authenticate, STRATEGY} from 'loopback4-authentication'; +import {authorize} from 'loopback4-authorization'; +import {CustomerDto} from '../models/dto/customer-dto.model'; +import {InvoiceDto} from '../models/dto/invoice-dto.model'; +import {PaymentSourceDto} from '../models/dto/payment-dto.model'; +import {PermissionKey} from '../permissions'; +import {BillingCustomerRepository} from '../repositories/billing-customer.repository'; + +const basePath = '/billing-invoice'; +export class BillingInvoiceController { + constructor( + @repository(BillingCustomerRepository) + public billingCustomerRepository: BillingCustomerRepository, + @inject(BillingComponentBindings.BillingProvider) + private readonly billingProvider: IService< + CustomerDto, + InvoiceDto, + PaymentSourceDto, + CustomerDto, + InvoiceDto, + PaymentSourceDto + >, + ) {} + + @authorize({ + permissions: [PermissionKey.CreateBillingInvoice], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @post(basePath, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.OK]: { + description: 'invoice model instance', + content: {'application/json': {schema: getModelSchemaRef(InvoiceDto)}}, + }, + }, + }) + async create( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(InvoiceDto, { + title: 'newInvoice', + exclude: ['id', 'status'], + }), + }, + }, + }) + invoiceDto: Omit, + ): Promise { + const customer = await this.billingCustomerRepository.find({ + where: {customerId: invoiceDto.customer_id}, + }); + + if (customer.length == 0) { + throw new Error(' Customer with tenantId is not present'); + } + const invoice = await this.billingProvider.createInvoice(invoiceDto); + await this.billingCustomerRepository.updateById(customer[0].id, { + invoiceId: invoice.id, + invoiceStatus: invoice.status, + }); + return invoice; + } + + @authorize({ + permissions: [PermissionKey.GetBillingInvoice], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @get(`${basePath}/{invoiceId}`, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.OK]: { + description: 'get invoice', + content: {'application/json': {schema: getModelSchemaRef(InvoiceDto)}}, + }, + }, + }) + async getInvoice( + @param.path.string('invoiceId') invoiceId: string, + ): Promise { + return this.billingProvider.retrieveInvoice(invoiceId); + } + + @authorize({ + permissions: [PermissionKey.UpdateBillingInvoice], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @patch(`${basePath}/{invoiceId}`, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.NO_CONTENT]: { + description: 'Billing Invoice PATCH success', + }, + }, + }) + async updateById( + @param.path.string('invoiceId') invoiceId: string, + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(InvoiceDto, {partial: true}), + }, + }, + }) + invoiceDto: Partial, + ): Promise { + await this.billingProvider.updateInvoice(invoiceId, invoiceDto); + } + + @authorize({ + permissions: [PermissionKey.CreateBillingInvoice], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @post(`${basePath}/{invoiceId}/payments`, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.OK]: { + description: 'invoice model instance', + }, + }, + }) + async applyPaymentForInvoice( + @param.path.string('invoiceId') invoiceId: string, + @param.header.string('paymentSourceId') paymentSourceId: string, + ): Promise { + await this.billingProvider.applyPaymentSourceForInvoice( + invoiceId, + paymentSourceId, + ); + } + + @authorize({ + permissions: [PermissionKey.DeleteBillingPaymentSource], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @del(`${basePath}/{invoiceId}`, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.NO_CONTENT]: { + description: 'Billing Invoice DELETE success', + }, + }, + }) + async deleteById( + @param.path.string('invoiceId') invoiceId: string, + ): Promise { + const customer = await this.billingCustomerRepository.find({ + where: {invoiceId: invoiceId}, + }); + + if (customer.length == 0) { + throw new Error(' Customer with tenantId is not present'); + } + await this.billingProvider.deleteInvoice(invoiceId); + await this.billingCustomerRepository.updateById(customer[0].id, { + invoiceId: undefined, + invoiceStatus: undefined, + }); + } +} diff --git a/services/subscription-service/src/controllers/billing-payment-source.controller.ts b/services/subscription-service/src/controllers/billing-payment-source.controller.ts new file mode 100644 index 0000000..9eb5c29 --- /dev/null +++ b/services/subscription-service/src/controllers/billing-payment-source.controller.ts @@ -0,0 +1,134 @@ +import {inject} from '@loopback/core'; +import {repository} from '@loopback/repository'; +import { + del, + get, + getModelSchemaRef, + param, + post, + requestBody, +} from '@loopback/rest'; +import {OPERATION_SECURITY_SPEC, STATUS_CODE} from '@sourceloop/core'; +import {BillingComponentBindings, IService} from 'local-billing'; +import {authenticate, STRATEGY} from 'loopback4-authentication'; +import {authorize} from 'loopback4-authorization'; +import {CustomerDto} from '../models/dto/customer-dto.model'; +import {InvoiceDto} from '../models/dto/invoice-dto.model'; +import {PaymentSourceDto} from '../models/dto/payment-dto.model'; +import {PermissionKey} from '../permissions'; +import {BillingCustomerRepository} from '../repositories/billing-customer.repository'; + +const basePath = '/billing-payment-source'; +export class BillingPaymentSourceController { + constructor( + @repository(BillingCustomerRepository) + public billingCustomerRepository: BillingCustomerRepository, + @inject(BillingComponentBindings.BillingProvider) + private readonly billingProvider: IService< + CustomerDto, + InvoiceDto, + PaymentSourceDto, + CustomerDto, + InvoiceDto, + PaymentSourceDto + >, + ) {} + + @authorize({ + permissions: [PermissionKey.CreateBillingPaymentSource], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @post(basePath, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.OK]: { + description: 'Payment model instance', + content: { + 'application/json': {schema: getModelSchemaRef(PaymentSourceDto)}, + }, + }, + }, + }) + async create( + @requestBody({ + content: { + 'application/json': { + schema: getModelSchemaRef(PaymentSourceDto, { + title: 'NewPaymentSource', + exclude: ['id'], + }), + }, + }, + }) + paymentSourceDto: PaymentSourceDto, + ): Promise { + const customer = await this.billingCustomerRepository.find({ + where: {customerId: paymentSourceDto.customer_id}, + }); + + if (customer.length == 0) { + throw new Error(' Customer with tenantId is not present'); + } + const paymentSource = + await this.billingProvider.createPaymentSource(paymentSourceDto); + await this.billingCustomerRepository.updateById(customer[0].id, { + paymentSourceId: paymentSource.id, + }); + return paymentSource; + } + + @authorize({ + permissions: [PermissionKey.GetBillingPaymentSource], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @get(basePath, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.OK]: { + description: 'get payment source', + content: { + 'application/json': {schema: getModelSchemaRef(PaymentSourceDto)}, + }, + }, + }, + }) + async getPaymentSource( + @param.path.string('paymentSourceId') paymentSourceId: string, + ): Promise { + return this.billingProvider.retrievePaymentSource(paymentSourceId); + } + + @authorize({ + permissions: [PermissionKey.DeleteBillingPaymentSource], + }) + @authenticate(STRATEGY.BEARER, { + passReqToCallback: true, + }) + @del(`${basePath}/{paymentSourceId}`, { + security: OPERATION_SECURITY_SPEC, + responses: { + [STATUS_CODE.NO_CONTENT]: { + description: 'Billing Payment Source DELETE success', + }, + }, + }) + async deleteById( + @param.path.string('paymentSourceId') paymentSourceId: string, + ): Promise { + const customer = await this.billingCustomerRepository.find({ + where: {paymentSourceId: paymentSourceId}, + }); + + if (customer.length == 0) { + throw new Error(' Customer with tenantId is not present'); + } + await this.billingProvider.deletePaymentSource(paymentSourceId); + await this.billingCustomerRepository.updateById(customer[0].id, { + paymentSourceId: undefined, + }); + } +} diff --git a/services/subscription-service/src/controllers/webhook.controller.ts b/services/subscription-service/src/controllers/webhook.controller.ts new file mode 100644 index 0000000..3474847 --- /dev/null +++ b/services/subscription-service/src/controllers/webhook.controller.ts @@ -0,0 +1,43 @@ +import {intercept} from '@loopback/core'; +import {repository} from '@loopback/repository'; +import {post, requestBody} from '@loopback/rest'; +import {authorize} from 'loopback4-authorization'; +import {WEBHOOK_VERIFIER} from '../keys'; +import {BillingCustomerRepository} from '../repositories/billing-customer.repository'; + +export class WebhookController { + constructor( + @repository(BillingCustomerRepository) + public billingCustomerRepository: BillingCustomerRepository, + ) {} + + @authorize({ + permissions: ['*'], + }) + @intercept(WEBHOOK_VERIFIER) + @post('/webhooks/chargebee') + async handleWebhook(@requestBody() payload: any): Promise { + const event = payload.event_type; + const content = payload.content; + + switch (event) { + case 'payment_succeeded': + await this.handlePaymentSucceeded(content); + break; + // Handle other events here + default: + console.log(`Unhandled event type: ${event}`); + } + } + + private async handlePaymentSucceeded(content: any): Promise { + const paymentDetails = content.transaction; + const customerId = paymentDetails.customer_id; + const customer = await this.billingCustomerRepository.find({ + where: {customerId: customerId}, + }); + await this.billingCustomerRepository.updateById(customer[0].id, { + invoiceStatus: content.invoice.status, + }); + } +} diff --git a/services/subscription-service/src/interceptors/index.ts b/services/subscription-service/src/interceptors/index.ts new file mode 100644 index 0000000..dc511f6 --- /dev/null +++ b/services/subscription-service/src/interceptors/index.ts @@ -0,0 +1 @@ +export * from './webhook-verifier.interceptor'; diff --git a/services/subscription-service/src/interceptors/webhook-verifier.interceptor.ts b/services/subscription-service/src/interceptors/webhook-verifier.interceptor.ts new file mode 100644 index 0000000..c3773ea --- /dev/null +++ b/services/subscription-service/src/interceptors/webhook-verifier.interceptor.ts @@ -0,0 +1,46 @@ +import { + Interceptor, + InvocationContext, + Provider, + Setter, + ValueOrPromise, + inject, +} from '@loopback/core'; +import {HttpErrors, RequestContext} from '@loopback/rest'; +import {ILogger, LOGGER} from '@sourceloop/core'; +import {AuthenticationBindings, IAuthUser} from 'loopback4-authentication'; + +export class WebhookVerifierProvider implements Provider { + constructor( + @inject(LOGGER.LOGGER_INJECT) + private readonly logger: ILogger, + @inject.setter(AuthenticationBindings.CURRENT_USER) + private readonly setCurrentUser: Setter, + ) {} + + value() { + return this.intercept.bind(this); + } + + async intercept( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, + ) { + const {request} = invocationCtx.parent as RequestContext; + const authHeader = request.headers['authorization']; + const username = process.env.WEBHOOK_USERNAME; + const password = process.env.WEBHOOK_PASSWORD; + const expectedAuthHeader = + 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); + + try { + if (!authHeader || authHeader !== expectedAuthHeader) { + throw new HttpErrors.Unauthorized('Invalid authorization.'); + } + } catch (e) { + this.logger.error(e); + throw new HttpErrors.Unauthorized(); + } + return next(); + } +} diff --git a/services/subscription-service/src/keys.ts b/services/subscription-service/src/keys.ts index 3801d39..74aabbc 100644 --- a/services/subscription-service/src/keys.ts +++ b/services/subscription-service/src/keys.ts @@ -1,4 +1,4 @@ -import {BindingKey} from '@loopback/core'; +import {BindingKey, Interceptor} from '@loopback/core'; import {ISubscriptionServiceConfig} from './types'; import {BINDING_PREFIX} from '@sourceloop/core'; import {VerifyFunction} from 'loopback4-authentication'; @@ -10,6 +10,10 @@ export namespace SubscriptionServiceBindings { ); } +export const WEBHOOK_VERIFIER = BindingKey.create( + 'sf.webhook.verifier', +); + export const LEAD_TOKEN_VERIFIER = BindingKey.create< VerifyFunction.BearerFn >('sf.user.lead.verifier'); diff --git a/services/subscription-service/src/models/billing-customer.model.ts b/services/subscription-service/src/models/billing-customer.model.ts new file mode 100644 index 0000000..fe7cb6c --- /dev/null +++ b/services/subscription-service/src/models/billing-customer.model.ts @@ -0,0 +1,51 @@ +import {model, property} from '@loopback/repository'; +import {UserModifiableEntity} from '@sourceloop/core'; +@model({ + name: 'billing_customer', + description: 'contacts belonging to a tenant', +}) +export class BillingCustomer extends UserModifiableEntity { + @property({ + type: 'string', + id: true, + generated: true, + }) + id?: string; + + @property({ + type: 'string', + name: 'tenant_id', + required: true, + }) + tenantId: string; // tenantId of customer + + @property({ + type: 'string', + name: 'customer_id', + required: true, + }) + customerId: string; // id of customer generated on third party billing module + + @property({ + type: 'string', + name: 'payment_source_id', + }) + paymentSourceId?: string; + + @property({ + type: 'string', + name: 'invoice_id', + }) + invoiceId?: string; + + @property({ + type: 'boolean', + name: 'invoice_status', + description: 'payment or invoice status', + }) + invoiceStatus?: Boolean; + + constructor(data?: Partial) { + super(data); + } +} diff --git a/services/subscription-service/src/models/dto/address-dto.model.ts b/services/subscription-service/src/models/dto/address-dto.model.ts new file mode 100644 index 0000000..7542f67 --- /dev/null +++ b/services/subscription-service/src/models/dto/address-dto.model.ts @@ -0,0 +1,80 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class AddressDto extends Entity { + @property({ + type: 'string', + name: 'first_name', + }) + first_name: string; + + @property({ + type: 'string', + name: 'last_name', + }) + last_name: string; + @property({ + type: 'string', + required: true, + }) + email: string; + + @property({ + type: 'string', + }) + company?: string; + + @property({ + type: 'string', + }) + phone?: string; + + @property({ + type: 'string', + }) + line1?: string; + + @property({ + type: 'string', + }) + line2?: string; + + @property({ + type: 'string', + }) + line3?: string; + + @property({ + type: 'string', + required: true, + }) + city: string; + + @property({ + type: 'string', + required: true, + }) + state: string; + + @property({ + type: 'string', + required: true, + }) + zip: string; + + @property({ + type: 'string', + required: true, + }) + country: string; + + constructor(data?: Partial) { + super(data); + } +} + +export interface AddressRelations { + // describe navigational properties here +} + +export type AddressWithRelations = AddressDto & AddressRelations; diff --git a/services/subscription-service/src/models/dto/charge-dto.model.ts b/services/subscription-service/src/models/dto/charge-dto.model.ts new file mode 100644 index 0000000..87c4aaf --- /dev/null +++ b/services/subscription-service/src/models/dto/charge-dto.model.ts @@ -0,0 +1,26 @@ +import {Entity, model, property} from '@loopback/repository'; + +@model() +export class ChargeDto extends Entity { + @property({ + type: 'number', + required: true, + }) + amount: number; + + @property({ + type: 'string', + required: true, + }) + description: string; + + constructor(data?: Partial) { + super(data); + } +} + +export interface ChargeRelations { + // describe navigational properties here +} + +export type ChargeWithRelations = ChargeDto & ChargeRelations; diff --git a/services/subscription-service/src/models/dto/customer-dto.model.ts b/services/subscription-service/src/models/dto/customer-dto.model.ts new file mode 100644 index 0000000..889945c --- /dev/null +++ b/services/subscription-service/src/models/dto/customer-dto.model.ts @@ -0,0 +1,69 @@ +import {model, Model, property} from '@loopback/repository'; +import {AddressDto} from './address-dto.model'; + +@model({ + name: 'customer_dto', +}) +export class CustomerDto extends Model { + @property({ + type: 'string', + name: 'id', + }) + id?: string; + + @property({ + type: 'string', + name: 'first_name', + }) + first_name: string; + + @property({ + type: 'string', + name: 'last_name', + }) + last_name: string; + + @property({ + type: 'string', + name: 'email', + }) + email: string; + + @property({ + type: 'string', + name: 'company', + }) + company: string; + + @property({ + type: 'string', + name: 'phone', + }) + phone: string; + + @property({ + type: AddressDto, + name: 'billing_address', + }) + billing_address: AddressDto; + + constructor(data?: Partial) { + super(data); + } +} + +// this refers to the billing address and shipping address. +export interface IAddress { + firstName: string; + lastName: string; + email: string; + company?: string; + phone?: string; + line1?: string; + line2?: string; + line3?: string; + city: string; + state: string; + zip: string; + coutnry: string; +} diff --git a/services/subscription-service/src/models/dto/index.ts b/services/subscription-service/src/models/dto/index.ts new file mode 100644 index 0000000..3037b0e --- /dev/null +++ b/services/subscription-service/src/models/dto/index.ts @@ -0,0 +1,5 @@ +export * from './address-dto.model'; +export * from './charge-dto.model'; +export * from './customer-dto.model'; +export * from './invoice-dto.model'; +export * from './payment-dto.model'; diff --git a/services/subscription-service/src/models/dto/invoice-dto.model.ts b/services/subscription-service/src/models/dto/invoice-dto.model.ts new file mode 100644 index 0000000..02171fc --- /dev/null +++ b/services/subscription-service/src/models/dto/invoice-dto.model.ts @@ -0,0 +1,55 @@ +import {model, Model, property} from '@loopback/repository'; +import {AddressDto} from './address-dto.model'; +import {ChargeDto} from './charge-dto.model'; + +@model({ + name: 'invoice_dto', +}) +export class InvoiceDto extends Model { + @property({ + type: 'string', + name: 'id', + }) + id?: string; + + @property({ + type: 'string', + name: 'customer_id', + }) + customer_id: string; + + @property({ + type: AddressDto, + name: 'shipping_address', + }) + shipping_address: AddressDto; + + @property({ + type: 'array', + itemType: ChargeDto, + name: 'charges', + }) + charges: ChargeDto[]; + + @property({ + type: 'string', + name: 'auto_collection', + }) + auto_collection?: string; + + @property({ + type: 'string', + name: 'status', + }) + status?: string; + + constructor(data?: Partial) { + super(data); + } +} + +// This refers to the item to be added in invoice. +// export interface ICharge{ +// amount:number; +// description:string; +// } diff --git a/services/subscription-service/src/models/dto/payment-dto.model.ts b/services/subscription-service/src/models/dto/payment-dto.model.ts new file mode 100644 index 0000000..96d9a2a --- /dev/null +++ b/services/subscription-service/src/models/dto/payment-dto.model.ts @@ -0,0 +1,55 @@ +import {model, Model, property} from '@loopback/repository'; + +@model({ + name: 'payment_source_dto', +}) +export class PaymentSourceDto extends Model { + @property({ + type: 'string', + name: 'id', + }) + id?: string; + + @property({ + type: 'string', + name: 'customer_id', + }) + customer_id: string; + + @property({ + type: 'object', + name: 'card', + required: true, + jsonSchema: { + type: 'object', + properties: { + gateway_account_id: {type: 'string'}, + number: {type: 'string'}, + expiry_month: {type: 'number'}, + expiry_year: {type: 'number'}, + cvv: {type: 'number'}, + }, + required: [ + 'gateway_account_id', + 'number', + 'expiry_month', + 'expiry_year', + 'cvv', + ], + }, + }) + card: ICard; + + constructor(data?: Partial) { + super(data); + } +} + +// this refers to the card +export interface ICard { + gatewayAccountId: string; + number: string; + expiryMonth: number; + expiryYear: number; + cvv: number; +} diff --git a/services/subscription-service/src/models/index.ts b/services/subscription-service/src/models/index.ts index 1685546..3257c8a 100644 --- a/services/subscription-service/src/models/index.ts +++ b/services/subscription-service/src/models/index.ts @@ -5,3 +5,4 @@ export * from './service.model'; export * from './resource.model'; export * from './billing-cycle.model'; export * from './currency.model'; +export * from './dto'; diff --git a/services/subscription-service/src/permissions.ts b/services/subscription-service/src/permissions.ts index 6b75b20..923737e 100644 --- a/services/subscription-service/src/permissions.ts +++ b/services/subscription-service/src/permissions.ts @@ -45,4 +45,16 @@ export const PermissionKey = { DeleteInvoice: '10214', ViewInvoice: '10215', CreateNotification: '2', + CreateBillingCustomer: '5321', + CreateBillingPaymentSource: '5322', + CreateBillingInvoice: '5323', + GetBillingCustomer: '5324', + GetBillingPaymentSource: '5325', + GetBillingInvoice: '5326', + UpdateBillingCustomer: '5327', + UpdateBillingPaymentSource: '5328', + UpdateBillingInvoice: '5329', + DeleteBillingCustomer: '5331', + DeleteBillingPaymentSource: '5332', + DeleteBillingInvoice: '5333', }; diff --git a/services/subscription-service/src/repositories/billing-customer.repository.ts b/services/subscription-service/src/repositories/billing-customer.repository.ts new file mode 100644 index 0000000..3569174 --- /dev/null +++ b/services/subscription-service/src/repositories/billing-customer.repository.ts @@ -0,0 +1,24 @@ +import {Getter, inject} from '@loopback/core'; +import {juggler} from '@loopback/repository'; +import { + DefaultUserModifyCrudRepository, + IAuthUserWithPermissions, +} from '@sourceloop/core'; +import {AuthenticationBindings} from 'loopback4-authentication'; +import {BillingCustomer} from '../models/billing-customer.model'; +import {SubscriptionDbSourceName} from '../types'; + +export class BillingCustomerRepository extends DefaultUserModifyCrudRepository< + BillingCustomer, + typeof BillingCustomer.prototype.id, + {} +> { + constructor( + @inject(`datasources.${SubscriptionDbSourceName}`) + dataSource: juggler.DataSource, + @inject.getter(AuthenticationBindings.CURRENT_USER) + public readonly getCurrentUser: Getter, + ) { + super(BillingCustomer, dataSource, getCurrentUser); + } +}