Track stripe sessions and fulfill purchase status
parent
ea124b597d
commit
9265124956
|
|
@ -1,9 +1,9 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AccessController } from './access.controller';
|
import { AccessController } from './controller/access.controller';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './service/user.service';
|
||||||
import { BuyController } from './buy.controller';
|
import { BuyController } from './controller/buy.controller';
|
||||||
import { PurchaseService } from './purchase.service';
|
import { PurchaseService } from './service/purchase.service';
|
||||||
import { StripeService } from './stripe.service';
|
import { StripeService } from './service/stripe.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { Body, Controller, Get, Post, Redirect } from '@nestjs/common';
|
|
||||||
import { PurchaseService } from './purchase.service';
|
|
||||||
import { UserService } from './user.service';
|
|
||||||
import { PurchaseItem } from './dto/purchase-item';
|
|
||||||
import { StripeService } from './stripe.service';
|
|
||||||
|
|
||||||
@Controller('/buy')
|
|
||||||
export class BuyController {
|
|
||||||
constructor(
|
|
||||||
private readonly purchaseService: PurchaseService,
|
|
||||||
private readonly userService: UserService,
|
|
||||||
private readonly stripeService: StripeService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Post('/start')
|
|
||||||
@Redirect()
|
|
||||||
public async startPurchaseFlow(
|
|
||||||
@Body('userId') userId: number,
|
|
||||||
@Body('quantity') quantity: number,
|
|
||||||
) {
|
|
||||||
const user = this.userService.getUserById(userId);
|
|
||||||
const purchase = this.purchaseService.recordPurchaseStart(
|
|
||||||
user,
|
|
||||||
PurchaseItem.PAELLA,
|
|
||||||
quantity,
|
|
||||||
);
|
|
||||||
const session = await this.stripeService.createStripeSession(purchase);
|
|
||||||
console.log(session);
|
|
||||||
return { url: session.url };
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('/complete')
|
|
||||||
public complete() {}
|
|
||||||
|
|
||||||
@Get('/cancel')
|
|
||||||
public cancel() {}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
Post,
|
Post,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { UserService } from './user.service';
|
import { UserService } from '../service/user.service';
|
||||||
|
|
||||||
@Controller('/access')
|
@Controller('/access')
|
||||||
export class AccessController {
|
export class AccessController {
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Body, Controller, Get, Post, Redirect } from '@nestjs/common';
|
||||||
|
import { PurchaseService } from '../service/purchase.service';
|
||||||
|
import { UserService } from '../service/user.service';
|
||||||
|
import { PurchaseItem } from '../dto/purchase-item';
|
||||||
|
import { StripeService } from '../service/stripe.service';
|
||||||
|
import { PurchaseStatus } from 'src/dto/purchase-status';
|
||||||
|
|
||||||
|
@Controller('/buy')
|
||||||
|
export class BuyController {
|
||||||
|
constructor(
|
||||||
|
private readonly purchaseService: PurchaseService,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
private readonly stripeService: StripeService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('/start')
|
||||||
|
public async startPurchaseFlow(
|
||||||
|
@Body('userId') userId: number,
|
||||||
|
@Body('quantity') quantity: number,
|
||||||
|
) {
|
||||||
|
const user = this.userService.getUserById(userId);
|
||||||
|
const purchase = this.purchaseService.recordPurchaseStart(
|
||||||
|
user,
|
||||||
|
PurchaseItem.PAELLA,
|
||||||
|
quantity,
|
||||||
|
);
|
||||||
|
const session = await this.stripeService.createStripeSession(
|
||||||
|
user,
|
||||||
|
purchase,
|
||||||
|
);
|
||||||
|
this.purchaseService.attachStripeSessionToPurchase(session.id, purchase.id);
|
||||||
|
console.log(session);
|
||||||
|
return { url: session.url };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/complete')
|
||||||
|
public async complete(@Body('userId') userId: number) {
|
||||||
|
const user = this.userService.getUserById(userId);
|
||||||
|
const session = await this.stripeService.getStripeSessionForCustomer(user);
|
||||||
|
const isPaid = session.payment_status == 'paid';
|
||||||
|
this.purchaseService.findAndTransitionPurchaseStatus(
|
||||||
|
session.id,
|
||||||
|
isPaid ? PurchaseStatus.COMPLETED : PurchaseStatus.IN_PROGRESS,
|
||||||
|
);
|
||||||
|
return { url: '/signup' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
export enum PurchaseStatus {
|
export enum PurchaseStatus {
|
||||||
CREATED = 'CREATED',
|
CREATED = 'CREATED',
|
||||||
|
IN_PROGRESS = 'IN_PROGRESS',
|
||||||
COMPLETED = 'COMPLETED',
|
COMPLETED = 'COMPLETED',
|
||||||
FAILED = 'FAILED',
|
FAILED = 'FAILED',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,5 @@ export class Purchase {
|
||||||
purchasedProduct: PurchaseItem;
|
purchasedProduct: PurchaseItem;
|
||||||
purchasedUnits: number;
|
purchasedUnits: number;
|
||||||
status: PurchaseStatus;
|
status: PurchaseStatus;
|
||||||
|
stripeSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { Purchase } from './dto/purchase';
|
|
||||||
import { User } from './dto/user';
|
|
||||||
import { PurchaseItem } from './dto/purchase-item';
|
|
||||||
import { PurchaseStatus } from './dto/purchase-status';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PurchaseService {
|
|
||||||
private readonly purchases: Purchase[] = [];
|
|
||||||
private readonly logger = new Logger(PurchaseService.name);
|
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
public recordPurchaseStart(
|
|
||||||
user: User,
|
|
||||||
purchasedItem: PurchaseItem,
|
|
||||||
units: number,
|
|
||||||
): Purchase {
|
|
||||||
this.logger.debug(`Recording purchase for user ID: ${user.id}`);
|
|
||||||
const purchase: Purchase = {
|
|
||||||
id: this.purchases.length,
|
|
||||||
purchasedBy: user,
|
|
||||||
purchasedProduct: purchasedItem,
|
|
||||||
purchasedUnits: units,
|
|
||||||
status: PurchaseStatus.CREATED,
|
|
||||||
};
|
|
||||||
this.purchases.push(purchase);
|
|
||||||
return purchase;
|
|
||||||
}
|
|
||||||
|
|
||||||
public findPurchasesByUser(userId: number): Purchase[] {
|
|
||||||
this.logger.debug(`Searching purchases for user ID: ${userId}`);
|
|
||||||
return this.purchases.filter(
|
|
||||||
(purchase) => (purchase.purchasedBy.id = userId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
|
import { Purchase } from 'src/dto/purchase';
|
||||||
|
import { PurchaseItem } from 'src/dto/purchase-item';
|
||||||
|
import { PurchaseStatus } from 'src/dto/purchase-status';
|
||||||
|
import { User } from 'src/dto/user';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PurchaseService {
|
||||||
|
private readonly purchases: Purchase[] = [];
|
||||||
|
private readonly logger = new Logger(PurchaseService.name);
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
public recordPurchaseStart(
|
||||||
|
user: User,
|
||||||
|
purchasedItem: PurchaseItem,
|
||||||
|
units: number,
|
||||||
|
): Purchase {
|
||||||
|
this.logger.debug(`Recording purchase for user ID: ${user.id}`);
|
||||||
|
const purchase: Purchase = {
|
||||||
|
id: this.purchases.length,
|
||||||
|
purchasedBy: user,
|
||||||
|
purchasedProduct: purchasedItem,
|
||||||
|
purchasedUnits: units,
|
||||||
|
status: PurchaseStatus.CREATED,
|
||||||
|
};
|
||||||
|
this.purchases.push(purchase);
|
||||||
|
return purchase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public attachStripeSessionToPurchase(
|
||||||
|
sessionId: string,
|
||||||
|
purchaseId: number,
|
||||||
|
): void {
|
||||||
|
const purchase = this.purchases.find(
|
||||||
|
(purchase) => purchase.id === purchaseId,
|
||||||
|
);
|
||||||
|
if (!purchase) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
purchase.stripeSessionId = sessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public findAndTransitionPurchaseStatus(
|
||||||
|
sessionId: string,
|
||||||
|
purchaseStatus: PurchaseStatus,
|
||||||
|
) {
|
||||||
|
const purchase = this.purchases.find(
|
||||||
|
(purchase) => purchase.stripeSessionId === sessionId,
|
||||||
|
);
|
||||||
|
if (!purchase) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
purchase.status = purchaseStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public findPurchasesByUser(userId: number): Purchase[] {
|
||||||
|
this.logger.debug(`Searching purchases for user ID: ${userId}`);
|
||||||
|
return this.purchases.filter(
|
||||||
|
(purchase) => (purchase.purchasedBy.id = userId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { Purchase } from '../dto/purchase';
|
||||||
|
import Stripe from 'stripe';
|
||||||
|
import { User } from '../dto/user';
|
||||||
|
import { NotFoundError } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StripeService {
|
||||||
|
private readonly stripe: Stripe;
|
||||||
|
private readonly stripeSessions: Map<number, string> = new Map();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY!;
|
||||||
|
this.stripe = new Stripe(stripePrivateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createStripeSession(
|
||||||
|
user: User,
|
||||||
|
purchase: Purchase,
|
||||||
|
): Promise<Stripe.Response<Stripe.Checkout.Session>> {
|
||||||
|
const session = await this.stripe.checkout.sessions.create({
|
||||||
|
line_items: [
|
||||||
|
{
|
||||||
|
price: 'price_1SWLunBQui1OXGptCHHnBvLX',
|
||||||
|
quantity: purchase.purchasedUnits,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mode: 'payment',
|
||||||
|
success_url: 'http://localhost:5173/buy/return',
|
||||||
|
cancel_url: 'http://localhost:5173/buy/return',
|
||||||
|
payment_method_types: ['card', 'paypal'],
|
||||||
|
});
|
||||||
|
this.stripeSessions.set(user.id, session.id);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStripeSessionForCustomer(
|
||||||
|
user: User,
|
||||||
|
): Promise<Stripe.Response<Stripe.Checkout.Session>> {
|
||||||
|
const sessionId = this.stripeSessions.get(user.id);
|
||||||
|
if (!sessionId) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
return this.stripe.checkout.sessions.retrieve(sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||||
import { User } from './dto/user';
|
import { User } from 'src/dto/user';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { Purchase } from './dto/purchase';
|
|
||||||
import Stripe from 'stripe';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class StripeService {
|
|
||||||
private readonly stripe: Stripe;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY!;
|
|
||||||
this.stripe = new Stripe(stripePrivateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public createStripeSession(purchase: Purchase): Promise<Stripe.Response<Stripe.Checkout.Session>> {
|
|
||||||
return this.stripe.checkout.sessions.create({
|
|
||||||
line_items: [
|
|
||||||
{
|
|
||||||
price: purchase.purchasedProduct,
|
|
||||||
quantity: purchase.purchasedUnits,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
mode: 'payment',
|
|
||||||
success_url: 'https://localhost:5173/buy/complete',
|
|
||||||
cancel_url: 'https://localhost:5173/buy/cancel',
|
|
||||||
payment_method_types: ['card'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue