Skip to content

Feature/membership final refactor #287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 62 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
ffa6047
Implement final membership model (aligned with Haycoder’s schema)
DarinHajou Jun 19, 2025
ed0e258
Add IMembership to type definitions
DarinHajou Jun 19, 2025
68d7a31
Move membership model file to models directory
DarinHajou Jun 19, 2025
563057b
Add MembershipClassType enum for membership level
DarinHajou Jun 19, 2025
a85ebbd
Add membership controller and mark manageMembership as legacy
DarinHajou Jun 20, 2025
9ca1bd4
Set up membership.service.ts with core logic, schema updates, and mar…
DarinHajou Jun 20, 2025
32da7d5
Refactor: Rename MembershipType to MembershipClassType in membership …
DarinHajou Jun 20, 2025
981ac39
Add membership.routes.ts with legacy and current endpoints + Swagger …
DarinHajou Jun 20, 2025
98fe436
Add membership routes to app entry
DarinHajou Jun 20, 2025
766e9b0
Create isMembershipFound middleware to check active membership by Pi UID
DarinHajou Jun 20, 2025
491a0a7
Change name for membership class enum in types
DarinHajou Jun 20, 2025
9a5053b
Add membership API schema definitions
DarinHajou Jun 20, 2025
8235549
Add pi-backend.d.ts to resolve TS type errors
DarinHajou Jun 20, 2025
124d2b4
Comment out payment logic to avoid temp errors in membership service
DarinHajou Jun 20, 2025
ecd7f91
Fix pi import from platformApiCLient
DarinHajou Jun 20, 2025
cd92a0e
Refactor platformAPIclient to export axios instance and piNetwork sep…
DarinHajou Jun 20, 2025
9cc1b3e
Update tsconfig with custom typeRoots
DarinHajou Jun 20, 2025
fba7d48
Change file name to start with uppercase MembershipSchema.yml
DarinHajou Jun 23, 2025
bf8bed9
Remove exclamation marks from legacy note in swagger summary
DarinHajou Jun 24, 2025
023925d
Add missing user_id and pi_uid fields to new membership creation
DarinHajou Jun 24, 2025
59837c2
Add missing user_id and pi_uid fields to new membership creation
DarinHajou Jun 24, 2025
587faf0
Merge branch 'feature/membership-final-refactor' of https://github.co…
DarinHajou Jun 24, 2025
1754133
Refactor populate script to avoid duplicate memberships
DarinHajou Jun 24, 2025
d9af287
Add unique index to pi_uid in Membership schema
DarinHajou Jun 24, 2025
6f84adb
Use findById instead of findOne for membership by ID
DarinHajou Jun 24, 2025
523938c
Move manage membership to separate file membershipLegacy.controller.t…
DarinHajou Jun 24, 2025
4fc92e5
Move manageMembership function to separate file membershipLegacy.cont…
DarinHajou Jun 24, 2025
b4b2033
Merge branch 'feature/membership-final-refactor' of https://github.co…
DarinHajou Jun 24, 2025
9a691fb
Refactor route /manage to legacy membership controller for non-paymen…
DarinHajou Jun 24, 2025
51310e7
Move addOrUpdateMembership to separate file membershipLegacy.service.ts
DarinHajou Jun 24, 2025
2252e65
Rename addOrUpdateMembership to addOrUpdateMembershipLegacy for legac…
DarinHajou Jun 24, 2025
bf45e74
Update populateMemberships script connection logic
DarinHajou Jun 26, 2025
a1d8fb0
Add tierRank helper to support membership upgrade/downgrade logic
DarinHajou Jun 26, 2025
7101e0c
Add edge case logic to updateOrRenewMembership using tierRank
DarinHajou Jun 26, 2025
6355569
Delete redundant membership.ts helper file
DarinHajou Jun 26, 2025
2a83910
Add full membership tier handling logic (upgrade, downgrade, renewal)
DarinHajou Jun 26, 2025
214398f
Enforce max membership duration per class in updateOrRenewMembership()
DarinHajou Jun 26, 2025
2aa20a1
Finalize membership tier logic with pi_uid (pre-user_id switch)
DarinHajou Jun 27, 2025
9d1fe9b
Declare currentUser on Express Request interface for type safety
DarinHajou Jun 27, 2025
968a133
Update updateOrRenewMembership controller to use req.currentUser inst…
DarinHajou Jun 27, 2025
5429530
Swap legacy manageMembership route with updateOrRenewMembership
DarinHajou Jun 27, 2025
7d36ee5
Refactor membership service to use user object instead of pi_uid
DarinHajou Jun 27, 2025
bc63516
Update category switching and required mappi value
DarinHajou Jun 27, 2025
385ffee
Finalize membership logic with full tier/category handling, renewal r…
DarinHajou Jun 27, 2025
97613c4
Integrate membership processing in payment flow
DarinHajou Jun 28, 2025
9a96abf
Add membership metadata field to IPayment model
DarinHajou Jun 28, 2025
c025d1d
Include metadata in Payment schema
DarinHajou Jun 28, 2025
0a08730
Implement updateOrRenewMembershipAfterPayment to handle Pi U2A member…
DarinHajou Jun 28, 2025
1c95314
Add _id field to IUser to support ObjectId access in services
DarinHajou Jun 29, 2025
b1d9248
Add metadata to NewPayment interface for Order and Membership payment…
DarinHajou Jun 29, 2025
545300c
Make pi_payment_id optional and sparse to support Pi U2A pre-approval…
DarinHajou Jun 29, 2025
0f0b9b5
Remove redundant import from membership routes
DarinHajou Jun 29, 2025
9c42fc0
Refactor Express.Request typing to use global namespace
DarinHajou Jun 29, 2025
86d907c
Fix ObjectId to string conversion for payment and order creation
DarinHajou Jun 29, 2025
fbe9f50
Feat: Add payment initiation handler and refactor payment controller …
DarinHajou Jun 29, 2025
2466c0d
Feat: Add createU2APayment service for Pi U2A flow and fix metadata h…
DarinHajou Jun 29, 2025
4327ea2
Add createU2APayment service for Pi U2A flow and fix metadata handlin…
DarinHajou Jun 29, 2025
085e93e
Merge branch 'feature/membership-final-refactor' of https://github.co…
DarinHajou Jun 29, 2025
6198182
Add /payments/initiate route for Pi U2A payment initiation
DarinHajou Jun 29, 2025
b3c5cb5
Prevent duplicate pending membership payments in createU2APayment
DarinHajou Jul 6, 2025
2d27c02
Merge remote-tracking branch 'origin/dev' into feature/membership-fin…
swoocn Jul 14, 2025
1709adf
Misc PR adjustment; mod unit test; WIP.
swoocn Jul 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @DarinHajou - I noticed that package-lock.json was committed in the PR, but package.json wasn't modified.
Just checking—was committing the lock file intentional on your end?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @swoocn. Sry for the late reply, but - no that wasn't intentional on my part.

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

68 changes: 68 additions & 0 deletions src/config/docs/MembershipSchema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
components:
schemas:
GetSingleMembershipRs:
type: object
properties:
pi_uid:
type: string
description: The Pi Network user ID (used as membership identifier)
example: pi_f392cb3b73913bcb2d1e5b69
membership_class:
$ref: '/api/docs/enum/MembershipClassType.yml#/components/schemas/MembershipClassType'
membership_expiration:
type: string
format: date-time
example: 2024-12-21T00:00:00.000Z
mappi_balance:
type: number
example: 100
mappi_used_to_date:
type: number
example: 25
_id:
type: string
example: 66741c62b175e7d059a2639e
__v:
type: number
example: 0

ManageMembershipRq:
type: object
properties:
membership_class:
$ref: '/api/docs/enum/MembershipClassType.yml#/components/schemas/MembershipClassType'
membership_duration:
type: number
example: 4
mappi_allowance:
type: number
example: 100
required:
- membership_class
- membership_duration
- mappi_allowance

ManageMembershipRs:
type: object
properties:
pi_uid:
type: string
example: pi_f392cb3b73913bcb2d1e5b69
membership_class:
$ref: '/api/docs/enum/MembershipClassType.yml#/components/schemas/MembershipClassType'
membership_expiration:
type: string
format: date-time
example: 2024-12-21T00:00:00.000Z
mappi_balance:
type: number
example: 200
mappi_used_to_date:
type: number
example: 0
_id:
type: string
example: 66741c62b175e7d059a2639e
__v:
type: number
example: 0
6 changes: 2 additions & 4 deletions src/config/platformAPIclient.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 😎👍

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from "axios";
import PiNetwork from 'pi-backend';
import Pi from "pi-backend";
import { env } from "../utils/env";

export const platformAPIClient = axios.create({
Expand All @@ -13,6 +13,4 @@ export const platformAPIClient = axios.create({
const apiKey = env.PI_API_KEY || '';
const walletSeed = env.WALLET_PRIVATE_SEED || '';

const pi = new PiNetwork(apiKey, walletSeed);

export default pi;
export const piNetwork = new Pi(apiKey, walletSeed);
45 changes: 45 additions & 0 deletions src/controllers/membershipController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Request, Response } from "express";
import * as membershipService from "../services/membership.service";
import logger from "../config/loggingConfig";

// Controller to fetch a single membership by its ID
export const getSingleMembership = async (req: Request, res: Response) => {
const { membership_id } = req.params;

try {
const currentMembership = await membershipService.getSingleMembershipById(membership_id);

if (!currentMembership) {
logger.warn(`Membership with ID ${membership_id} not found.`);
return res.status(404).json({ message: "Membership not found" });
}

logger.info(`Fetched membership with ID ${membership_id}`);
return res.status(200).json(currentMembership);
} catch (error) {
logger.error(`Error getting membership ID ${membership_id}:`, error);
return res.status(500).json({ message: 'An error occurred while getting single membership; please try again later' });
}
};

export const updateOrRenewMembership = async (req: Request, res: Response) => {
if (!req.currentUser) {
return res.status(401).json({ error: "Unauthorized - user not authenticated "});
}

try {
const { membership_class, membership_duration, mappi_allowance } = req.body;

const updated = await membershipService.updateOrRenewMembership({
user: req.currentUser,
membership_class,
membership_duration,
mappi_allowance,
});

return res.status(200).json(updated);
} catch (error: any) {
logger.error("Failed to update or renew membership:", error);
return res.status(400).json({ error: error.message });
}
};
47 changes: 47 additions & 0 deletions src/controllers/membershipLegacy.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Request, Response } from "express";
import * as membershipLegacyService from "../services/membershipLegacy.service";
import logger from "../config/loggingConfig";
import { MembershipClassType } from "../models/enums/membershipClassType";

// Controller to add or update a user's membership
// !!! Legacy controller — used only for manual upgrades and testing.
// Membership upgrades should now go through Pi (U2A) payment flow.
export const manageMembership = async (req: Request, res: Response) => {
const authUser = req.currentUser;

// Early return if request is not authenticated
if (!authUser) {
logger.warn('Unauthorized attempt to manage membership.');
return res.status(401).json({ error: 'Unauthorized' });
}

const { membership_class, membership_duration, mappi_allowance } = req.body;

// Basic input validation
if (
!membership_class ||
!Object.values(MembershipClassType).includes(membership_class) || // Ensure membership_class is valid
typeof membership_duration !== 'number' ||
typeof mappi_allowance !== 'number'
) {
return res.status(400).json({ error: 'Invalid request payload' });
}

try {
// Add or update membership in the service layer
const updatedMembership = await membershipLegacyService.addOrUpdateMembershipLegacy(
authUser,
membership_class,
membership_duration,
mappi_allowance
);

logger.info(`Membership managed successfully for user ${authUser.pi_uid}`);
return res.status(200).json(updatedMembership);
} catch (error) {
logger.error(`Failed to manage membership for user ${authUser.pi_uid}:`, error);
return res.status(500).json({
message: 'An error occurred while getting single membership; please try again later',
});
}
};
47 changes: 43 additions & 4 deletions src/controllers/paymentController.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,66 @@
import { Request, Response } from "express";
import logger from "../config/loggingConfig";
import {
import {
processIncompletePayment,
processPaymentApproval,
processPaymentCancellation,
processPaymentCompletion
} from "../helpers/payment";
import { IUser } from "../types";
import { IUser, PaymentDataType } from "../types";
import { createU2APayment } from "../services/payment.service";

export const onPaymentInitiation = async (req: Request, res: Response) => {
try {
const currentUser = req.currentUser as IUser;
const paymentData = req.body as PaymentDataType;

const { memo, amount, metadata } = paymentData;

if (!memo || !amount || !metadata?.payment_type) {
return res.status(400).json({ message: "Missing required payment fields" });
}

const newPayment = await createU2APayment(currentUser, paymentData);

if (!newPayment) {
return res.status(500).json({ success: false, message: "Failed to initiate payment" });
}

res.status(200).json({
success: true,
message: "Payment created successfully",
payment_id: newPayment._id,
});
} catch (error: any) {
logger.error("Payment initiation failed", { error });
res.status(500).json({ success: false, message: "Failed to initiate payment" });
}
};

export const onIncompletePaymentFound = async (req: Request, res: Response) => {
const { payment } = req.body;
const { payment } = req.body;

if (!payment || typeof payment !== "object" || !payment.identifier) {
logger.warn("Invalid or missing payment payload in onIncompletePaymentFound", { payment });
return res.status(400).json({
success: false,
message: "Invalid payment data provided.",
});
}

try {
const processedPayment = await processIncompletePayment(payment);
return res.status(200).json(processedPayment);
} catch (error) {
logger.error(`Failed to process incomplete payment for paymentID ${ payment.identifier }:`, error);
logger.error(`Failed to process incomplete payment for paymentID ${payment.identifier}:`, error);
return res.status(500).json({
success: false,
message: 'An error occurred while processing incomplete payment; please try again later'
});
}
};


export const onPaymentApproval = async (req: Request, res: Response) => {
const currentUser = req.currentUser as IUser;
const { paymentId } = req.body;
Expand Down
30 changes: 0 additions & 30 deletions src/helpers/membership.ts

This file was deleted.

39 changes: 25 additions & 14 deletions src/helpers/payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '../services/order.service';
import { IUser, NewOrder, PaymentDataType, PaymentInfo } from '../types';
import logger from '../config/loggingConfig';
import { updateOrRenewMembershipAfterPayment } from '../services/membership.service';

function buildPaymentData(
piPaymentId: string,
Expand Down Expand Up @@ -79,7 +80,8 @@ const checkoutProcess = async (
}

// Construct payment data object for recording the transaction
const paymentData = buildPaymentData(piPaymentId, buyer._id as string, currentPayment);
const paymentData = buildPaymentData(piPaymentId, buyer._id.toString()
, currentPayment);
// Create a new payment record
const newPayment = await createPayment(paymentData)
// Validate payment record creation succeeded
Expand All @@ -96,7 +98,7 @@ const checkoutProcess = async (

// Construct order data object
const orderData = buildOrderData(
buyer._id as string,
buyer._id.toString(),
seller._id as string,
newPayment._id as string,
currentPayment
Expand Down Expand Up @@ -183,21 +185,30 @@ export const processPaymentApproval = async (
};
}

// Handle logic based on the payment type
if (currentPayment?.metadata.payment_type === PaymentType.BuyerCheckout) {
const newOrder = await checkoutProcess(paymentId, currentUser, currentPayment);
logger.info("Order created successfully: ", newOrder._id);
} else if (currentPayment?.metadata.payment_type === PaymentType.Membership) {
logger.info("Membership subscription processed successfully");
// Handle logic based on the payment type
if (currentPayment?.metadata.payment_type === PaymentType.BuyerCheckout) {
const newOrder = await checkoutProcess(paymentId, currentUser, currentPayment);
logger.info("Order created successfully: ", newOrder._id);

} else if (currentPayment?.metadata.payment_type === PaymentType.Membership) {
const metadata = currentPayment.metadata.MembershipPayment;

if (!metadata || !metadata.membership_id) {
throw new Error("Missing or invalid membership metadata");
}

// Approve the payment on the Pi platform
await platformAPIClient.post(`/v2/payments/${ paymentId }/approve`);
await updateOrRenewMembershipAfterPayment(currentPayment);
logger.info(`Membership subscription processed successfully for pi_uid: ${metadata.pi_uid}`);
}

// Approve the payment on the Pi platform
await platformAPIClient.post(`/v2/payments/${ paymentId }/approve`);

return {
success: true,
message: `Payment approved with id ${ paymentId }`,
};

return {
success: true,
message: `Payment approved with id ${ paymentId }`,
};
} catch (error: any) {
if (error.response) {
logger.error("platformAPIClient error", {
Expand Down
Loading