Skip to content

Commit

Permalink
feat: Add base endpoints for Stripe integration [DEV-3671] (#500)
Browse files Browse the repository at this point in the history
* Add base endpoints for Stripe integration

* Add swagger docs and make it as separate swagger for admin and api

* - Add Stripe webhook handling
- Add subscription entity
- Add chekout API guarding

* Add auth routes to admin

* Lint and format

* swagger changes

* Get rid of /admin/checkout and move it to /admin/subscriptions

* Change endpoint in swagger

* Fix swagger generation and add API routes

* Fix swagger request body for subscription update

* Fix stripeSync function

* Get permissions from the M2M token

* Sync updated and  small clean-ups

* Clean-ups

* Add trial period days to subscription creation

* Get also trilaing subsriptions

* Get rd of Stripe naming

* Small cleeanups and refactoring

* Make swagger changes

* Resolve review comments

* Overall format + lockfile

---------

Co-authored-by: Tasos Derisiotis <[email protected]>
  • Loading branch information
Andrew Nikitin and Eengineer1 authored Apr 11, 2024
1 parent e20d83d commit 99792c7
Show file tree
Hide file tree
Showing 78 changed files with 7,025 additions and 4,253 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ some tokens on the testnet for making the process simpler.
2. `FAUCET_URI`: Faucet service API endpoint (Default: `https://faucet-api.cheqd.network/credit`)
3. `TESTNET_MINIMUM_BALANCE`: Minimum balance on account before it is automatically topped up from the faucet. This value should be expressed as an integer in `CHEQ` tokens, which will then be converted in the background to `ncheq` denomination. Account balance check is carried out on every account creation/login. (Default: 10,000 CHEQ testnet tokens)

#### Stripe integration

The application supports Stripe integration for payment processing.

1. `STRIPE_ENABLED` - Enable/disable Stripe integration (`false` by default)
2. `STRIPE_SECRET_KEY` - Secret key for Stripe API. Please, keep it secret on deploying
3. `STRIPE_PUBLISHABLE_KEY` - Publishable key for Stripe API.
4. `STRIPE_WEBHOOK_SECRET` - Secret for Stripe Webhook.

### 3rd Party Connectors

The app supports 3rd party connectors for credential storage and delivery.
Expand Down
12 changes: 12 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ ARG ENABLE_ACCOUNT_TOPUP=false
ARG FAUCET_URI=https://faucet-api.cheqd.network/credit
ARG TESTNET_MINIMUM_BALANCE=1000

# Stripe
ARG STRIPE_ENABLED=false
ARG STRIPE_SECRET_KEY
ARG STRIPE_PUBLISHABLE_KEY
ARG STRIPE_WEBHOOK_SECRET

# Environment variables: base configuration
ENV NPM_CONFIG_LOGLEVEL ${NPM_CONFIG_LOGLEVEL}
ENV PORT ${PORT}
Expand Down Expand Up @@ -123,6 +129,12 @@ ENV POLYGON_RPC_URL ${POLYGON_RPC_URL}
ENV VERIDA_PRIVATE_KEY ${VERIDA_PRIVATE_KEY}
ENV POLYGON_PRIVATE_KEY ${POLYGON_PRIVATE_KEY}

# Environment variables: Stripe
ENV STRIPE_SECRET_KEY ${STRIPE_SECRET_KEY}
ENV STRIPE_PUBLISHABLE_KEY ${STRIPE_PUBLISHABLE_KEY}
ENV STRIPE_WEBHOOK_SECRET ${STRIPE_WEBHOOK_SECRET}
ENV STRIPE_ENABLED ${STRIPE_ENABLED}

# Set ownership permissions
RUN chown -R node:node /home/node/app

Expand Down
17 changes: 15 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"scripts": {
"build": "npm run build:swagger && npm run build:app",
"build:app": "tsc",
"build:swagger": "swagger-jsdoc --definition src/static/swagger-options.json -o src/static/swagger.json ./src/controllers/*.ts ./src/types/swagger-types.ts",
"build:swagger-api": "swagger-jsdoc --definition src/static/swagger-api-options.json -o src/static/swagger-api.json ./src/controllers/api/*.ts ./src/types/swagger-api-types.ts",
"build:swagger-admin": "swagger-jsdoc --definition src/static/swagger-admin-options.json -o src/static/swagger-admin.json ./src/controllers/admin/*.ts ./src/types/swagger-admin-types.ts",
"build:swagger": "npm run build:swagger-api && npm run build:swagger-admin",
"start": "node dist/index.js",
"start:local": "npm run build:app && npm run start",
"format": "prettier --write '**/*.{js,ts,cjs,mjs,json}'",
Expand Down Expand Up @@ -94,6 +96,7 @@
"pg-connection-string": "^2.6.2",
"secp256k1": "^5.0.0",
"sqlite3": "^5.1.7",
"stripe": "^14.18.0",
"swagger-ui-dist": "5.10.5",
"swagger-ui-express": "^5.0.0",
"typeorm": "^0.3.20",
Expand Down
88 changes: 77 additions & 11 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import path from 'path';
import swaggerUi from 'swagger-ui-express';
import { StatusCodes } from 'http-status-codes';

import { CredentialController } from './controllers/credential.js';
import { AccountController } from './controllers/account.js';
import { CredentialController } from './controllers/api/credential.js';
import { AccountController } from './controllers/api/account.js';
import { Authentication } from './middleware/authentication.js';
import { Connection } from './database/connection/connection.js';
import { CredentialStatusController } from './controllers/credential-status.js';
import { CredentialStatusController } from './controllers/api/credential-status.js';
import { CORS_ALLOWED_ORIGINS, CORS_ERROR_MSG } from './types/constants.js';
import { LogToWebHook } from './middleware/hook.js';
import { Middleware } from './middleware/middleware.js';
Expand All @@ -20,12 +20,17 @@ import * as dotenv from 'dotenv';
dotenv.config();

// Define Swagger file
import swaggerDocument from './static/swagger.json' assert { type: 'json' };
import { PresentationController } from './controllers/presentation.js';
import { KeyController } from './controllers/key.js';
import { DIDController } from './controllers/did.js';
import { ResourceController } from './controllers/resource.js';
import { FailedResponseTracker } from './middleware/event-tracker.js';
import swaggerAPIDocument from './static/swagger-api.json' assert { type: 'json' };
import swaggerAdminDocument from './static/swagger-admin.json' assert { type: 'json' };
import { PresentationController } from './controllers/api/presentation.js';
import { KeyController } from './controllers/api/key.js';
import { DIDController } from './controllers/api/did.js';
import { ResourceController } from './controllers/api/resource.js';
import { ResponseTracker } from './middleware/event-tracker.js';
import { ProductController } from './controllers/admin/product.js';
import { SubscriptionController } from './controllers/admin/subscriptions.js';
import { PriceController } from './controllers/admin/prices.js';
import { WebhookController } from './controllers/admin/webhook.js';

let swaggerOptions = {};
if (process.env.ENABLE_AUTHENTICATION === 'true') {
Expand Down Expand Up @@ -71,7 +76,7 @@ class App {
this.express.use(cookieParser());
const auth = new Authentication();
// EventTracking
this.express.use(new FailedResponseTracker().trackJson);
this.express.use(new ResponseTracker().trackJson);
// Authentication
if (process.env.ENABLE_AUTHENTICATION === 'true') {
this.express.use(
Expand All @@ -96,7 +101,19 @@ class App {
}
this.express.use(express.text());

this.express.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerDocument, swaggerOptions));
this.express.use(
'/swagger',
swaggerUi.serveFiles(swaggerAPIDocument, swaggerOptions),
swaggerUi.setup(swaggerAPIDocument, swaggerOptions)
);
if (process.env.STRIPE_ENABLED === 'true') {
this.express.use(
'/admin/swagger',
swaggerUi.serveFiles(swaggerAdminDocument),
swaggerUi.setup(swaggerAdminDocument)
);
this.express.use(Middleware.setStripeClient);
}
this.express.use(auth.handleError);
this.express.use(async (req, res, next) => await auth.accessControl(req, res, next));
}
Expand Down Expand Up @@ -206,6 +223,55 @@ class App {
express.static(path.join(process.cwd(), '/dist'), { extensions: ['js'], index: false })
);

// Portal
// Product
if (process.env.STRIPE_ENABLED === 'true') {
app.get(
'/admin/product/list',
ProductController.productListValidator,
new ProductController().listProducts
);
app.get(
'/admin/product/get/:productId',
ProductController.productGetValidator,
new ProductController().getProduct
);

// Prices
app.get('/admin/price/list', PriceController.priceListValidator, new PriceController().getListPrices);

// Subscription
app.post(
'/admin/subscription/create',
SubscriptionController.subscriptionCreateValidator,
new SubscriptionController().create
);
app.post(
'/admin/subscription/update',
SubscriptionController.subscriptionUpdateValidator,
new SubscriptionController().update
);
app.get('/admin/subscription/get', new SubscriptionController().get);
app.get(
'/admin/subscription/list',
SubscriptionController.subscriptionListValidator,
new SubscriptionController().list
);
app.delete(
'/admin/subscription/cancel',
SubscriptionController.subscriptionCancelValidator,
new SubscriptionController().cancel
);
app.post(
'/admin/subscription/resume',
SubscriptionController.subscriptionResumeValidator,
new SubscriptionController().resume
);

// Webhook
app.post('/admin/webhook', new WebhookController().handleWebhook);
}

// 404 for all other requests
app.all('*', (_req, res) => res.status(StatusCodes.BAD_REQUEST).send('Bad request'));
}
Expand Down
27 changes: 27 additions & 0 deletions src/controllers/admin/portal-customer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as dotenv from 'dotenv';
import type { Request, Response } from 'express';
import { check } from 'express-validator';
import { validationResult } from '../validator';
import type { PortalCustomerGetUnsuccessfulResponseBody } from '../../types/portal.js';

dotenv.config();

export class PortalCustomerController {
static portalCustomerGetValidator = [
check('logToUserId').optional().isString().withMessage('logToUserId should be a string').bail(),
];

async get(request: Request, response: Response) {
const result = validationResult(request);
// handle error
if (!result.isEmpty()) {
return response.status(400).json({
error: result.array().pop()?.msg,
} satisfies PortalCustomerGetUnsuccessfulResponseBody);
}

return response.status(500).json({
error: 'Not implemented yet',
});
}
}
73 changes: 73 additions & 0 deletions src/controllers/admin/prices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { Stripe } from 'stripe';
import type { Request, Response } from 'express';
import * as dotenv from 'dotenv';
import type { PriceListResponseBody, PriceListUnsuccessfulResponseBody } from '../../types/portal.js';
import { StatusCodes } from 'http-status-codes';
import { check } from '../validator/index.js';
import { validate } from '../validator/decorator.js';

dotenv.config();

export class PriceController {
static priceListValidator = [
check('productId').optional().isString().withMessage('productId should be a string').bail(),
];

/**
* @openapi
*
* /admin/price/list:
* get:
* summary: Get a list of prices
* description: Get a list of prices
* tags: [Price]
* parameters:
* - in: query
* name: productId
* schema:
* type: string
* description: The product id. If passed - returns filtered by this product list of prices.
* required: false
* responses:
* 200:
* description: A list of prices
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/PriceListResponseBody'
* 400:
* $ref: '#/components/schemas/InvalidRequest'
* 401:
* $ref: '#/components/schemas/UnauthorizedError'
* 500:
* $ref: '#/components/schemas/InternalError'
* 404:
* $ref: '#/components/schemas/NotFoundError'
*/
@validate
async getListPrices(request: Request, response: Response) {
const stripe = response.locals.stripe as Stripe;
// Get query parameters
const productId = request.query.productId;

try {
// Fetch the list of prices
const prices = productId
? await stripe.prices.list({
product: productId as string,
active: true,
})
: await stripe.prices.list({
active: true,
});

return response.status(StatusCodes.OK).json({
prices: prices,
} satisfies PriceListResponseBody);
} catch (error) {
return response.status(500).json({
error: `Internal error: ${(error as Error)?.message || error}`,
} satisfies PriceListUnsuccessfulResponseBody);
}
}
}
Loading

0 comments on commit 99792c7

Please sign in to comment.