Skip to content

Feature/membership system refractor #220

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 60 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
09b5ba6
Create membership schema and populate membership fields
DarinHajou Dec 14, 2024
2175258
Create script to populate the membership collection in DB
DarinHajou Dec 14, 2024
7e96292
Implement controller logic for fetching membership status, upgrading …
DarinHajou Dec 14, 2024
1dea87f
Added service functions to retrieve membership status, upgrade member…
DarinHajou Dec 14, 2024
a4fd281
Added interface IMembership to type definitions
DarinHajou Dec 14, 2024
df85a62
Add enum for membershipType for type safety
DarinHajou Dec 14, 2024
311304c
Change membershipClass to be explicitly typed as MembershipType
DarinHajou Dec 14, 2024
a9e0834
Update Membership schema to use MembershipType Enum
DarinHajou Dec 14, 2024
9fe5371
Validate the new membership class
DarinHajou Dec 14, 2024
e6e5fb5
Fix typo in membership_class field
DarinHajou Dec 21, 2024
9c0fd88
Add membership routes with endpoints for fetching, upgrading, and usi…
DarinHajou Dec 21, 2024
d0d64dc
Integrate membership routes into app.ts
DarinHajou Dec 21, 2024
151158a
Improve membership controller with better validation and pi_uid retri…
DarinHajou Dec 21, 2024
cbd0888
Modify import paths to reflect name change for Membership model
DarinHajou Jan 3, 2025
1e1e0df
Add membership YAML schema for API documentation
DarinHajou Jan 3, 2025
0c6220a
Reference YAML in membership routes
DarinHajou Jan 3, 2025
304351e
Add script for populating tier data to the DB
DarinHajou Jan 3, 2025
213c142
Indexing for faster queries on user_id
DarinHajou Jan 3, 2025
dcac618
Add Tier model for membership system
DarinHajou Jan 3, 2025
110f5dd
Improve membership service with better validation and streamlined logic
DarinHajou Jan 3, 2025
33da0ac
Add service layer for fetching membership tiers
DarinHajou Jan 3, 2025
bf65a41
Add tier controller for handling API responses
DarinHajou Jan 3, 2025
53911bd
Refractor the system to use pi_uid instead uf user_id as the key iden…
DarinHajou Jan 13, 2025
26882f7
Setup the Tier routes
DarinHajou Jan 13, 2025
ea2717a
Add Tier routes in app.ts
DarinHajou Jan 13, 2025
f7e6b09
Fix typo in 'Membership.ts' file
DarinHajou Jan 15, 2025
cabf7d4
Remove incorrectly cased memberShipType file
DarinHajou Jan 16, 2025
1b80cd8
Remove incorrectly cased membership.ts
DarinHajou Jan 16, 2025
aa47cb3
Recreate Membership.ts
DarinHajou Jan 16, 2025
f0f1f28
Recreate Membership.ts
DarinHajou Jan 16, 2025
5829820
Remove corrupted Membership.ts, add fixed version
DarinHajou Jan 16, 2025
02b968b
Add MembershipFixed.ts
DarinHajou Jan 16, 2025
c485d49
Rename MembershipFixed.ts to Membership.ts
DarinHajou Jan 16, 2025
659ae0b
Indent MembershipType import
DarinHajou Jan 16, 2025
ff913eb
Refractor Membership.ts to use pi_uid
DarinHajou Jan 17, 2025
0207091
Activate scripts folder.
swoocn Jan 26, 2025
2f0e4c4
Misc PR adjustments; WIP.
swoocn Feb 3, 2025
785d753
Merge remote-tracking branch 'origin/dev' into feature/membership-sys…
swoocn Feb 5, 2025
f703d18
Misc PR adjustments; restructured controller and service layers.
swoocn Feb 9, 2025
e90c69b
Misc PR adjustments; addtl. changes; WIP.
swoocn Feb 9, 2025
81bee59
Misc PR adjustments; added unit tests; WIP.
swoocn Feb 10, 2025
74e66bc
Misc PR adjustments; addtl. unit tests; WIP.
swoocn Feb 12, 2025
c71d498
Resolve TS Type Casting error.
swoocn Feb 12, 2025
9a8cfe2
Misc PR adjustments; addtl. unit tests; WIP.
swoocn Feb 13, 2025
b8aba93
PR adjustments based on Swagger API testing; Membership operations; WIP.
swoocn Feb 13, 2025
a04ad66
PR adjustments based on Swagger API testing; Transaction operations.
swoocn Feb 13, 2025
70d8e60
Resolve build failure due to broken unit test.
swoocn Feb 13, 2025
c4a5727
Merge remote-tracking branch 'origin/dev' into feature/membership-sys…
swoocn Mar 7, 2025
de50517
Merge remote-tracking branch 'origin/dev' into feature/membership-sys…
swoocn Mar 24, 2025
0c1d509
Refactor: unify membership field naming across codebase
DarinHajou Apr 1, 2025
4300352
Add validation for missing user in authenticateUser in userController
DarinHajou Apr 1, 2025
907dc2a
Add input validation and enum checks to membershipController
DarinHajou Apr 1, 2025
2527c2e
Implement transactional logic with expiry and mappi handling in membe…
DarinHajou Apr 1, 2025
ade39dc
Remove unused req.token in verifyToken middleware
DarinHajou Apr 1, 2025
7d75a02
Copy and paste the latest version of isPioneerFound from dev branch
DarinHajou Apr 1, 2025
db9e1f6
Add script to remove duplicate users by pi_uid from User collection
DarinHajou Apr 1, 2025
fd62eb5
Merge branch 'feature/membership-system-refractor' of https://github.…
DarinHajou Apr 1, 2025
336ab37
Add mock test for transactionService
DarinHajou Apr 1, 2025
1eaa1a3
Refractor 'getSingleMembership' error message to match specs
DarinHajou Apr 3, 2025
78a80c7
Refactor membership service tests to mock Mongoose session, findOne, …
DarinHajou Apr 3, 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: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@

# local misc
uploads
/scripts
49 changes: 49 additions & 0 deletions scripts/Memberships/populateMembership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import mongoose from "mongoose";
import dotenv from "dotenv";
import Membership from "../../src/models/Membership";
import User from "../../src/models/User";

dotenv.config();

const populateMemberships = async () => {
try {
// Connect to the database
await mongoose.connect(process.env.MONGO_URI || "mongodb+srv://mapofpi:[email protected]/map-of-pi?retryWrites=true&w=majority");

// Fetch all users
const users = await User.find({});
if (!users.length) {
console.log("No users found. Aborting.");
return;
}

// Iterates over each user and create a Membership entry
for (const user of users) {
console.log(`Processing user: ${user.pi_uid}`);

const existingMembership = await Membership.findOne({ membership_id: user.pi_uid });
if (existingMembership) {
console.log(`Membership already exists for user ID: ${user.pi_uid}. Skipping.`);
continue;
}

const membership = new Membership({
membership_id: user.pi_uid,
membership_class: "Casual",
membership_expiry_date: null,
mappi_balance: 0
});

await membership.save();
console.log(`Created membership for user ID: ${user.pi_uid}`);
}

console.log("Memberships created successfully for all users!");
} catch (error) {
console.error("Error populating memberships:", error);
} finally {
mongoose.connection.close();
}
};

populateMemberships();
39 changes: 39 additions & 0 deletions scripts/Regions/sanctioned-regions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import mongoose from 'mongoose';
import * as dotenv from 'dotenv';
import SanctionedRegion from '../../src/models/misc/SanctionedRegion';
import { RestrictedAreaBoundaries } from '../../src/models/enums/restrictedArea';

dotenv.config();

// insert sanctioned areas into MongoDB
const insertSanctionedRegions = async () => {
const mongoUri = 'mongodb+srv://mapofpi:[email protected]/map-of-pi?retryWrites=true&w=majority';
if (!mongoUri) {
console.error("MongoDB connection string is not defined in the environment variables.");
return;
}

try {
console.log('Attempting to connect to MongoDB...');
await mongoose.connect(mongoUri);
console.log("Connected to MongoDB");

const regions = Object.entries(RestrictedAreaBoundaries).map(([key, boundary]) => ({
location: key,
boundary,
}));

await SanctionedRegion.deleteMany(); // clear existing data
await SanctionedRegion.insertMany(regions); // insert new data

console.log('Inserted sanctioned regions into MongoDB');
} catch (error) {
console.error("Error inserting sanctioned regions:", (error as Error).message);
} finally {
await mongoose.disconnect();
console.log('Disconnected from MongoDB');
}
};

// Call the function to insert sanctioned regions
insertSanctionedRegions();
177 changes: 177 additions & 0 deletions scripts/Sellers/sellers-w-reviews.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import mongoose from 'mongoose';
import * as dotenv from 'dotenv';
import Seller from '../../src/models/Seller';
import ReviewFeedback from '../../src/models/ReviewFeedback';
import { SellerType } from '../../src/models/enums/sellerType';
import { RatingScale } from '../../src/models/enums/ratingScale';
import { IReviewFeedback, ISeller } from '../../src/types';

dotenv.config();

const numberOfSanctionedSellers = 1;
const maxReviewsPerSeller = 5;

const regions = [
// { name: "Mexico", minLat: 14.532866, maxLat: 32.718655, minLng: -118.455148, maxLng: -86.703392 }, // Mexico general bounds
// { name: "Mexico City", minLat: 19.2000, maxLat: 19.6000, minLng: -99.3650, maxLng: -98.9000 }, // Mexico City bounds
{ name: "Cuba", minLat: 19.4, maxLat: 23.7, minLng: -85.3, maxLng: -73.8 },
{ name: "Iran", minLat: 24.0, maxLat: 40.5, minLng: 43.0, maxLng: 63.5 },
{ name: "North Korea", minLat: 37.5, maxLat: 43.0, minLng: 123.5, maxLng: 131.2 },
{ name: "Syria", minLat: 32.0, maxLat: 37.5, minLng: 35.5, maxLng: 42.5 },
{ name: "Republic of Crimea", minLat: 43.8, maxLat: 46.4, minLng: 32.1, maxLng: 36.8 },
{ name: "Donetsk Oblast", minLat: 46.6, maxLat: 49.3, minLng: 36.2, maxLng: 39.1 },
{ name: "Luhansk Oblast", minLat: 47.7, maxLat: 50.1, minLng: 37.7, maxLng: 40.3 },
];

// Create an array of the valid RatingScale values
const ratingValues = [RatingScale.DESPAIR, RatingScale.SAD, RatingScale.OKAY, RatingScale.HAPPY, RatingScale.DELIGHT];

const getRandomCoordinates = (regionName: string) => {
const region = regions.find(r => r.name === regionName);
if (!region) throw new Error(`Region "${regionName}" not found`);
const lat = Math.random() * (region.maxLat - region.minLat) + region.minLat;
const lng = Math.random() * (region.maxLng - region.minLng) + region.minLng;
return [lng, lat];
};

const getRandomRating = () => {
const randomIndex = Math.floor(Math.random() * ratingValues.length);
return ratingValues[randomIndex];
};

const createSeller = (index: number, region: string) => {
return new Seller({
seller_id: `${region}_0ss0ss0ss-0ss0ss-0ss0ss_${index}`,
name: `${region} Mock Sanctioned Seller ${index}`,
seller_type: SellerType.Test,
description: `Description for ${region} Mock Sanctioned Seller ${index}`,
image: 'https://res.cloudinary.com/dcdcqbdsj/image/upload/v1729512851/map-of-pi-deactivated_j5qvqp.png',
address: `Location in ${region}`,
average_rating: mongoose.Types.Decimal128.fromString("5"),
sell_map_center: {
type: "Point",
coordinates: getRandomCoordinates(region)
},
});
}

const createReview = (sellerId: string, allSellerIds: string[]) => {
// Filter out the receiver_sellerId from the list of potential givers
const otherSellers = allSellerIds.filter(id => id !== sellerId);

// Randomly select a sellerId from the remaining sellers
const giver_sellerId = otherSellers[Math.floor(Math.random() * otherSellers.length)];

return new ReviewFeedback({
review_receiver_id: sellerId,
review_giver_id: giver_sellerId,
reply_to_review_id: null,
comment: `This is a mock review left by seller ${giver_sellerId}`,
rating: getRandomRating(),
review_date: new Date()
});
};

// Insert sellers into MongoDB
const insertSellers = async () => {
const mongoUri = 'mongodb+srv://mapofpi:[email protected]/map-of-pi?retryWrites=true&w=majority';
if (!mongoUri) {
console.error("MongoDB connection string is not defined in the environment variables.");
return;
}

try {
console.log('Attempting to connect to MongoDB...');
await mongoose.connect(mongoUri);
console.log("Connected to MongoDB");

const sellers: ISeller[] = [];

// Generate sellers in Sanctioned Regions
for (let i = 1; i <= numberOfSanctionedSellers; i++) {
const seller = createSeller(i, "Cuba");
sellers.push(seller);
}
for (let i = 1; i <= numberOfSanctionedSellers; i++) {
const seller = createSeller(i, "Iran");
sellers.push(seller);
}
for (let i = 1; i <= numberOfSanctionedSellers; i++) {
const seller = createSeller(i, "North Korea");
sellers.push(seller);
}
for (let i = 1; i <= numberOfSanctionedSellers; i++) {
const seller = createSeller(i, "Syria");
sellers.push(seller);
}
for (let i = 1; i <= numberOfSanctionedSellers; i++) {
const seller = createSeller(i, "Republic of Crimea");
sellers.push(seller);
}
for (let i = 1; i <= numberOfSanctionedSellers; i++) {
const seller = createSeller(i, "Donetsk Oblast");
sellers.push(seller);
}
for (let i = 1; i <= numberOfSanctionedSellers; i++) {
const seller = createSeller(i, "Luhansk Oblast");
sellers.push(seller);
}

// Insert sellers into the Seller collection
await Seller.insertMany(sellers);
console.log('Inserted sellers into MongoDB');

// Generate reviews for each seller
const reviews: IReviewFeedback[] = [];
const allSellerIds = sellers.map(seller => (seller.toObject() as ISeller).seller_id);

sellers.forEach((seller) => {
const numReviews = Math.floor(Math.random() * maxReviewsPerSeller) + 1;
for (let i = 0; i < numReviews; i++) {
const review = createReview((seller.toObject() as ISeller).seller_id, allSellerIds);
reviews.push(review);
}
});

// Insert reviews into the Review collection
await ReviewFeedback.insertMany(reviews);
console.log('Inserted reviews into MongoDB');
} catch (error) {
console.error("Error inserting sellers/reviews:", (error as Error).message);
} finally {
await mongoose.disconnect();
console.log('Disconnected from MongoDB');
}
};

const deleteSanctionedSellersData = async () => {
const mongoUri = 'mongodb+srv://mapofpi:[email protected]/map-of-pi?retryWrites=true&w=majority';
if (!mongoUri) {
console.error("MongoDB connection string is not defined in the environment variables.");
return;
}

try {
console.log('Attempting to connect to MongoDB...');
await mongoose.connect(mongoUri);
console.log("Connected to MongoDB");

// Delete sellers with "Sanctioned" in their name
const sellerResult = await Seller.deleteMany({ name: /Sanctioned/i });
console.log(`${sellerResult.deletedCount} sellers removed.`);

// Delete reviews with specific review_receiver_id
const reviewResult = await ReviewFeedback.deleteMany({ review_receiver_id: /0ss0ss0ss/i });
console.log(`${reviewResult.deletedCount} review feedbacks removed.`);

await mongoose.disconnect();
} catch (error) {
console.error("Error removing sanctioned sellers data:", error);
}
};

// Call the function to insert sellers and reviews
insertSellers();

// Call the function to delete sellers and reviews
// deleteSanctionedSellersData();
26 changes: 26 additions & 0 deletions scripts/users/cleanUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import mongoose from "mongoose";
import dotenv from "dotenv";
import User from "../../src/models/User";
import { env } from "../../src/utils/env"

dotenv.config();

const run = async () => {
await mongoose.connect(env.MONGODB_URL);

const duplicates = await User.aggregate([
{ $group: { _id: "$pi_uid", count: { $sum: 1 }, ids: { $push: "$_id" } } },
{ $match: { count: { $gt: 1 } } },
]);

for (const dup of duplicates) {
const [, ...duplicateIds] = dup.ids; // keep the first, delete the rest
await User.deleteMany({ _id: { $in: duplicateIds } });
console.log(`Deleted ${duplicateIds.length} duplicate(s) for pi_uid: ${dup._id}`);
}

console.log("All duplicates removed.");
await mongoose.disconnect();
};

run();
65 changes: 65 additions & 0 deletions src/config/docs/MembershipsSchema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
components:
schemas:
GetSingleMembershipRs:
type: object
properties:
membership_id:
type: string
example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0
membership_class:
$ref: '/api/docs/enum/MembershipClassType.yml#/components/schemas/MembershipClassType'
membership_expiry_date:
type: string
format: date-time
example: 2024-12-21T00:00:00.000Z
mappi_balance:
type: number
example: 100
_id:
type: string
example: 66741c62b175e7d059a2639e
__v:
type: number
example: 0

ManageMembershipRq:
type: object
properties:
membership_id:
type: string
example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0
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_id
- membership_class
- membership_duration
- mappi_allowance

ManageMembershipRs:
type: object
properties:
membership_id:
type: string
example: 0d367ba3-a2e8-4380-86c3-ab7c0b7890c0
membership_class:
$ref: '/api/docs/enum/MembershipClassType.yml#/components/schemas/MembershipClassType'
membership_end_date:
type: string
format: date-time
example: 2024-12-21T00:00:00.000Z
mappi_balance:
type: number
example: 200
_id:
type: string
example: 66741c62b175e7d059a2639e
__v:
type: number
example: 0
Loading