From 997a688d32254d263efbe6ac24e8e6c854d28820 Mon Sep 17 00:00:00 2001 From: akrs-code Date: Fri, 12 Jun 2026 15:24:34 +0800 Subject: [PATCH 01/37] feat(models): add MVP mongoose schemas for auth, orders, payments, products, shops, users --- modules/auth/auth.model.js | 6 ++++ modules/orders/order.model.js | 52 +++++++++++++++++++++++++++++++ modules/payments/payment.model.js | 31 ++++++++++++++++++ modules/products/product.model.js | 25 +++++++++++++++ modules/shops/shop.model.js | 36 +++++++++++++++++++++ modules/users/user.model.js | 33 ++++++++++---------- 6 files changed, 167 insertions(+), 16 deletions(-) create mode 100644 modules/auth/auth.model.js create mode 100644 modules/orders/order.model.js create mode 100644 modules/payments/payment.model.js create mode 100644 modules/products/product.model.js create mode 100644 modules/shops/shop.model.js diff --git a/modules/auth/auth.model.js b/modules/auth/auth.model.js new file mode 100644 index 0000000..beda185 --- /dev/null +++ b/modules/auth/auth.model.js @@ -0,0 +1,6 @@ +// RefreshToken model removed for MVP. +// Using a single long-lived JWT (7d expiry) — sufficient for a demo/competition context. +// Re-add refresh token rotation post-competition when security hardening begins. + +// Placeholder to preserve the module folder structure. +module.exports = {}; diff --git a/modules/orders/order.model.js b/modules/orders/order.model.js new file mode 100644 index 0000000..3d1c3c0 --- /dev/null +++ b/modules/orders/order.model.js @@ -0,0 +1,52 @@ +const mongoose = require('mongoose'); + +// ── Item snapshot — price locked at time of order ───────────────────────── +const orderItemSchema = new mongoose.Schema( + { + product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true }, + name: { type: String, required: true }, // snapshot — safe against product edits + price: { type: Number, required: true }, // centavos snapshot + quantity: { type: Number, required: true, min: 1 }, + }, + { _id: false } +); + +const orderSchema = new mongoose.Schema( + { + // ── Parties ─────────────────────────────────────────────── + buyer: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + shop: { type: mongoose.Schema.Types.ObjectId, ref: 'Shop', required: true }, + + // ── Items & Total ───────────────────────────────────────── + items: [orderItemSchema], + total: { type: Number, required: true }, // centavos — sum of (price × qty) + + // ── Delivery ────────────────────────────────────────────── + deliveryAddress: { type: String, required: true }, + // Optional — seller fills in when handing off to a courier + trackingNumber: { type: String }, + courier: { type: String }, // e.g. 'LBC', 'J&T', 'Ninja Van', '2GO' + + // ── Payment ─────────────────────────────────────────────── + paymentMethod: { + type: String, + enum: ['cod', 'paymongo'], + default: 'cod', + }, + paymentId: { type: String }, // PayMongo payment link ID (null for COD) + + // ── Status Flow ─────────────────────────────────────────── + // COD: pending → confirmed → shipped → delivered | cancelled + // PayMongo: pending → confirmed (via webhook) → shipped → delivered | cancelled + status: { + type: String, + enum: ['pending', 'confirmed', 'shipped', 'delivered', 'cancelled'], + default: 'pending', + }, + }, + { + timestamps: true, + } +); + +module.exports = mongoose.model('Order', orderSchema); diff --git a/modules/payments/payment.model.js b/modules/payments/payment.model.js new file mode 100644 index 0000000..1b35cb4 --- /dev/null +++ b/modules/payments/payment.model.js @@ -0,0 +1,31 @@ +const mongoose = require('mongoose'); + +/** + * Tracks PayMongo payment links. + * COD orders do NOT create a Payment document — the order alone is sufficient. + */ +const paymentSchema = new mongoose.Schema( + { + order: { type: mongoose.Schema.Types.ObjectId, ref: 'Order', required: true, unique: true }, + buyer: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + + // ── PayMongo fields ─────────────────────────────────────── + paymongoLinkId: { type: String }, // links.id from PayMongo API response + checkoutUrl: { type: String }, // redirect buyer here to pay + amount: { type: Number }, // centavos + status: { + type: String, + enum: ['pending', 'paid', 'failed'], + default: 'pending', + }, + paidAt: { type: Date }, + + // Raw webhook payload stored for audit / debugging + webhookPayload: { type: mongoose.Schema.Types.Mixed, select: false }, + }, + { + timestamps: true, + } +); + +module.exports = mongoose.model('Payment', paymentSchema); diff --git a/modules/products/product.model.js b/modules/products/product.model.js new file mode 100644 index 0000000..8bd453c --- /dev/null +++ b/modules/products/product.model.js @@ -0,0 +1,25 @@ +const mongoose = require('mongoose'); + +const productSchema = new mongoose.Schema( + { + // ── Ownership ───────────────────────────────────────────── + shop: { type: mongoose.Schema.Types.ObjectId, ref: 'Shop', required: true, index: true }, + + // ── Core Info ───────────────────────────────────────────── + name: { type: String, required: true, trim: true }, + description: { type: String }, + price: { type: Number, required: true, min: 0 }, // stored in centavos (integer) + category: { type: String }, // optional tag + + // ── Media (S3 key: products/{productId}) ────────────────── + imageUrl: { type: String }, + + // ── Availability ───────────────────────────────────────── + inStock: { type: Boolean, default: true }, // seller toggles on/off + }, + { + timestamps: true, + } +); + +module.exports = mongoose.model('Product', productSchema); diff --git a/modules/shops/shop.model.js b/modules/shops/shop.model.js new file mode 100644 index 0000000..7a27266 --- /dev/null +++ b/modules/shops/shop.model.js @@ -0,0 +1,36 @@ +const mongoose = require('mongoose'); + +const shopSchema = new mongoose.Schema( + { + // ── Ownership ───────────────────────────────────────────── + owner: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + + // ── Core Info ───────────────────────────────────────────── + name: { type: String, required: true, trim: true }, + description: { type: String }, + category: { type: String }, // used for map filter tabs + + // ── Media (S3 keys: shops/{shopId}/cover, shops/{shopId}/logo) ──── + coverUrl: { type: String }, + logoUrl: { type: String }, + + // ── Location — GeoJSON Point (enables $near queries) ────── + location: { + type: { type: String, enum: ['Point'], default: 'Point' }, + coordinates: { type: [Number], default: undefined }, // [longitude, latitude] + }, + address: { type: String }, // human-readable display label + + // ── Visibility ──────────────────────────────────────────── + isActive: { type: Boolean, default: true }, // admin toggles to remove from map + }, + { + timestamps: true, + } +); + +// 2dsphere index powers /api/shops/nearby?lat=&lng=&radius= +shopSchema.index({ location: '2dsphere' }); +shopSchema.index({ category: 1, isActive: 1 }); + +module.exports = mongoose.model('Shop', shopSchema); diff --git a/modules/users/user.model.js b/modules/users/user.model.js index ca3e8af..b521a28 100644 --- a/modules/users/user.model.js +++ b/modules/users/user.model.js @@ -2,23 +2,24 @@ const mongoose = require('mongoose'); const userSchema = new mongoose.Schema( { - name: { - type: String, - required: true - }, - email: { - type: String, - required: true, - unique: true - }, - password: { - type: String, - required: true - }, - }, - { - timestamps: true + name: { type: String, required: true, trim: true }, + email: { type: String, required: true, unique: true, lowercase: true, trim: true }, + password: { type: String, required: true, select: false }, // bcrypt hash, hidden by default + role: { type: String, enum: ['buyer', 'seller', 'admin'], default: 'buyer' }, + avatarUrl: { type: String }, // S3 key: avatars/{userId} + isActive: { type: Boolean, default: true }, // admin can toggle to suspend + }, + { + timestamps: true, } ); +// Strip password from all JSON responses +userSchema.set('toJSON', { + transform: (_, ret) => { + delete ret.password; + return ret; + }, +}); + module.exports = mongoose.model('User', userSchema); From 73c658ff92793b1cf91428e4a10eb54205570490 Mon Sep 17 00:00:00 2001 From: akrs-code Date: Fri, 12 Jun 2026 15:24:56 +0800 Subject: [PATCH 02/37] feat(routes): add MVP route stubs for all modules (admin, auth, orders, payments, products, shops, users) --- modules/admin/admin.routes.js | 14 ++++++++++++++ modules/auth/auth.routes.js | 9 +++++++++ modules/orders/order.routes.js | 15 +++++++++++++++ modules/payments/payment.routes.js | 11 +++++++++++ modules/products/product.routes.js | 14 ++++++++++++++ modules/shops/shop.routes.js | 19 +++++++++++++++++++ modules/users/user.routes.js | 10 ++++++---- 7 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 modules/admin/admin.routes.js create mode 100644 modules/auth/auth.routes.js create mode 100644 modules/orders/order.routes.js create mode 100644 modules/payments/payment.routes.js create mode 100644 modules/products/product.routes.js create mode 100644 modules/shops/shop.routes.js diff --git a/modules/admin/admin.routes.js b/modules/admin/admin.routes.js new file mode 100644 index 0000000..8fe58bf --- /dev/null +++ b/modules/admin/admin.routes.js @@ -0,0 +1,14 @@ +// TODO: Implement admin routes +const express = require('express'); +const router = express.Router(); + +// All routes require: verifyToken + requireRole('admin') + +// GET /api/admin/stats — dashboard: total users, shops, orders, revenue +// GET /api/admin/users — list all users +// PUT /api/admin/users/:id/toggle — toggle user isActive (suspend / unsuspend) +// GET /api/admin/shops — list all shops +// PUT /api/admin/shops/:id/toggle — toggle shop isActive (show / hide from map) +// GET /api/admin/orders — read-only ledger of all orders + +module.exports = router; diff --git a/modules/auth/auth.routes.js b/modules/auth/auth.routes.js new file mode 100644 index 0000000..960a31c --- /dev/null +++ b/modules/auth/auth.routes.js @@ -0,0 +1,9 @@ +// TODO: Implement auth routes +const express = require('express'); +const router = express.Router(); + +// POST /api/auth/register — create account, assign role, return JWT +// POST /api/auth/login — verify credentials, return JWT + user object +// GET /api/auth/me — protected: return current user from token (session restore) + +module.exports = router; diff --git a/modules/orders/order.routes.js b/modules/orders/order.routes.js new file mode 100644 index 0000000..096c69d --- /dev/null +++ b/modules/orders/order.routes.js @@ -0,0 +1,15 @@ +// TODO: Implement orders routes +const express = require('express'); +const router = express.Router(); + +// Middleware: verifyToken, requireRole, isOrderOwner + +// POST /api/orders — buyer: place order (snapshot items, compute total) +// GET /api/orders/my — buyer: own order history +// GET /api/shops/:id/orders — seller: incoming orders for their shop +// PUT /api/orders/:id/confirm — seller: pending → confirmed +// PUT /api/orders/:id/ship — seller: confirmed → shipped (body: { trackingNumber, courier }) +// PUT /api/orders/:id/deliver — seller: shipped → delivered +// PUT /api/orders/:id/cancel — buyer or seller: cancel (only pre-shipped) + +module.exports = router; diff --git a/modules/payments/payment.routes.js b/modules/payments/payment.routes.js new file mode 100644 index 0000000..5d1b19f --- /dev/null +++ b/modules/payments/payment.routes.js @@ -0,0 +1,11 @@ +// TODO: Implement payments routes +const express = require('express'); +const router = express.Router(); + +// POST /api/payments/create-link — buyer: create PayMongo payment link for an order +// returns { checkoutUrl } → frontend redirects buyer +// POST /api/payments/webhook — PayMongo webhook: payment.paid event → order status: confirmed +// requires verifyWebhookSignature middleware +// GET /api/payments/:orderId/status — buyer: check current payment status for an order + +module.exports = router; diff --git a/modules/products/product.routes.js b/modules/products/product.routes.js new file mode 100644 index 0000000..eabdb91 --- /dev/null +++ b/modules/products/product.routes.js @@ -0,0 +1,14 @@ +// TODO: Implement products routes +const express = require('express'); +const router = express.Router(); + +// Middleware: verifyToken, requireRole('seller'), isShopOwner + +// GET /api/shops/:shopId/products — public: list products for a shop +// GET /api/products/:id — public: single product detail +// POST /api/shops/:shopId/products — seller: add product to shop +// PUT /api/products/:id — seller (isShopOwner): edit product details +// DELETE /api/products/:id — seller (isShopOwner): remove product +// POST /api/products/:id/upload — seller: get pre-signed S3 URL for product image + +module.exports = router; diff --git a/modules/shops/shop.routes.js b/modules/shops/shop.routes.js new file mode 100644 index 0000000..86bc728 --- /dev/null +++ b/modules/shops/shop.routes.js @@ -0,0 +1,19 @@ +// TODO: Implement shops routes +const express = require('express'); +const router = express.Router(); + +// Middleware: verifyToken, requireRole('seller'), isShopOwner + +// ── Shop routes ──────────────────────────────────────────────────────────── +// GET /api/shops — public: list shops (filter by category, isActive: true) +// GET /api/shops/nearby — public: geo-query ?lat=&lng=&radius= (uses 2dsphere $near) +// GET /api/shops/:id — public: shop detail + product list +// POST /api/shops — seller: create shop with coords → pin appears on map +// PUT /api/shops/:id — seller (isShopOwner): update shop info +// POST /api/shops/:id/upload — seller: get pre-signed S3 URL for cover/logo upload + +// ── Review sub-routes (nested under shops) ───────────────────────────────── +// GET /api/shops/:id/reviews — public: list reviews for a shop (seeded for demo) +// POST /api/shops/:id/reviews — buyer (verifyToken): submit a review + +module.exports = router; diff --git a/modules/users/user.routes.js b/modules/users/user.routes.js index 7274185..9a68adc 100644 --- a/modules/users/user.routes.js +++ b/modules/users/user.routes.js @@ -1,9 +1,11 @@ +// TODO: Implement user routes const express = require('express'); const router = express.Router(); -const { getUsers } = require('./user.controller'); -// Maps to /api/users/ -router.route('/') - .get(getUsers); +// All routes require verifyToken middleware + +// GET /api/users/profile — get own profile +// PUT /api/users/profile — update own name, avatarUrl +// POST /api/users/profile/upload — get pre-signed S3 URL for avatar upload module.exports = router; From 310fcc90187cb188ad8f84b7c935f91ec18f953f Mon Sep 17 00:00:00 2001 From: akrs-code Date: Fri, 12 Jun 2026 15:25:32 +0800 Subject: [PATCH 03/37] feat(controllers): add MVP controller stubs for all modules (admin, auth, orders, payments, products, shops, users) --- modules/admin/admin.controller.js | 10 ++++++++++ modules/auth/auth.controller.js | 7 +++++++ modules/orders/order.controller.js | 11 +++++++++++ modules/payments/payment.controller.js | 9 +++++++++ modules/products/product.controller.js | 10 ++++++++++ modules/shops/shop.controller.js | 16 ++++++++++++++++ modules/users/user.controller.js | 26 ++++++-------------------- 7 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 modules/admin/admin.controller.js create mode 100644 modules/auth/auth.controller.js create mode 100644 modules/orders/order.controller.js create mode 100644 modules/payments/payment.controller.js create mode 100644 modules/products/product.controller.js create mode 100644 modules/shops/shop.controller.js diff --git a/modules/admin/admin.controller.js b/modules/admin/admin.controller.js new file mode 100644 index 0000000..ae2ae8b --- /dev/null +++ b/modules/admin/admin.controller.js @@ -0,0 +1,10 @@ +// TODO: Implement admin controller +// Planned handlers: +// getStats — aggregate: total users, sellers, active shops, total orders, gross revenue +// getUsers — list all users (paginated) +// toggleUser — flip user.isActive true/false +// getShops — list all shops (paginated) +// toggleShop — flip shop.isActive (removes from / restores to map) +// getAllOrders — read-only paginated order ledger across all buyers/sellers + +module.exports = {}; diff --git a/modules/auth/auth.controller.js b/modules/auth/auth.controller.js new file mode 100644 index 0000000..94f3f3e --- /dev/null +++ b/modules/auth/auth.controller.js @@ -0,0 +1,7 @@ +// TODO: Implement auth controller +// Planned handlers: +// register — hash password (bcrypt), create user with role, return JWT +// login — compare hash, check isActive, return JWT + user object +// getMe — decode token, return current user (used to restore session on app load) + +module.exports = {}; diff --git a/modules/orders/order.controller.js b/modules/orders/order.controller.js new file mode 100644 index 0000000..3455ed7 --- /dev/null +++ b/modules/orders/order.controller.js @@ -0,0 +1,11 @@ +// TODO: Implement orders controller +// Planned handlers: +// placeOrder — buyer: create order from cart items (snapshot name+price), compute total +// getMyOrders — buyer: own order history +// getShopOrders — seller: list incoming orders for their shop +// confirmOrder — seller: pending → confirmed +// shipOrder — seller: confirmed → shipped (optionally save trackingNumber + courier) +// deliverOrder — seller: shipped → delivered (marks as complete) +// cancelOrder — buyer or seller: any pre-shipped status → cancelled + +module.exports = {}; diff --git a/modules/payments/payment.controller.js b/modules/payments/payment.controller.js new file mode 100644 index 0000000..c82cc05 --- /dev/null +++ b/modules/payments/payment.controller.js @@ -0,0 +1,9 @@ +// TODO: Implement payments controller +// Planned handlers: +// createCheckout — create a Stripe or PayMongo checkout session for an order +// stripeWebhook — handle Stripe webhook events (payment_intent.succeeded, etc.) +// paymongoWebhook — handle PayMongo webhook events +// getPaymentStatus — return payment status for a given order +// refundPayment — admin: trigger a refund via gateway API + +module.exports = {}; diff --git a/modules/products/product.controller.js b/modules/products/product.controller.js new file mode 100644 index 0000000..106bff9 --- /dev/null +++ b/modules/products/product.controller.js @@ -0,0 +1,10 @@ +// TODO: Implement products controller +// Planned handlers: +// getProducts — browse products (category, price, search, pagination) +// getProductBySlug — single product detail +// createProduct — seller: add product to catalog +// updateProduct — seller/owner: edit product +// deleteProduct — seller / admin: remove product +// moderateProduct — admin: approve or reject product listing + +module.exports = {}; diff --git a/modules/shops/shop.controller.js b/modules/shops/shop.controller.js new file mode 100644 index 0000000..9cfaf0a --- /dev/null +++ b/modules/shops/shop.controller.js @@ -0,0 +1,16 @@ +// TODO: Implement shops controller +// Planned handlers: +// +// ── Shop handlers ───────────────────────────────────────────────────────── +// getShops — list / search shops (category filter, isActive: true, pagination) +// getShopsNearby — geo-query: shops near [lng, lat] within radius ($near + 2dsphere) +// getShopById — single shop detail (includes product list) +// createShop — seller: create shop, set GeoJSON coordinates +// updateShop — seller/owner: update shop info +// getUploadUrl — return pre-signed S3 URL for cover/logo upload +// +// ── Review handlers (nested, accessed via /api/shops/:id/reviews) ────────── +// getReviews — public: list seeded reviews for a shop +// createReview — buyer: submit a 1–5 star review with optional comment + +module.exports = {}; diff --git a/modules/users/user.controller.js b/modules/users/user.controller.js index e04d0a5..0d3d4f3 100644 --- a/modules/users/user.controller.js +++ b/modules/users/user.controller.js @@ -1,21 +1,7 @@ -const User = require('./user.model'); +// TODO: Implement users controller +// Planned handlers: +// getMyProfile — return current user's profile (name, email, avatarUrl, role) +// updateMyProfile — update own name, avatarUrl +// getUploadUrl — return pre-signed S3 URL for avatar upload -// @desc Get all users -// @route GET /api/users -// @access Public -const getUsers = async (req, res, next) => { - try { - const users = await User.find().select('-password'); - res.status(200).json({ - status: 'success', - count: users.length, - data: users - }); - } catch (error) { - next(error); - } -}; - -module.exports = { - getUsers -}; +module.exports = {}; From 1e0b6ed492fdcddb7e66ecb1aea477a232645d8f Mon Sep 17 00:00:00 2001 From: akrs-code Date: Fri, 12 Jun 2026 15:25:55 +0800 Subject: [PATCH 04/37] feat(middleware): add auth middleware stubs (verifyToken, requireRole, isShopOwner, isOrderOwner, verifyWebhookSignature) --- middleware/.gitkeep | 0 middleware/auth.js | 62 +++++++++++++++++++++++++++++++++++++++ modules/auth/.gitkeep | 0 modules/payments/.gitkeep | 0 4 files changed, 62 insertions(+) delete mode 100644 middleware/.gitkeep create mode 100644 middleware/auth.js delete mode 100644 modules/auth/.gitkeep delete mode 100644 modules/payments/.gitkeep diff --git a/middleware/.gitkeep b/middleware/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..23f64de --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,62 @@ +// TODO: Implement auth middleware + +const jwt = require('jsonwebtoken'); + +/** + * verifyToken + * Checks Authorization: Bearer , decodes it, + * and attaches { id, role } to req.user. + * Usage: router.get('/protected', verifyToken, handler) + */ +const verifyToken = (req, res, next) => { + // TODO: implement + next(); +}; + +/** + * requireRole(...roles) + * Factory — checks req.user.role against the allowed list. + * Usage: router.get('/admin', verifyToken, requireRole('admin'), handler) + */ +const requireRole = (...roles) => (req, res, next) => { + // TODO: implement + next(); +}; + +/** + * isShopOwner + * Confirms shop.owner === req.user.id before allowing mutations. + * Expects :shopId or :id param that resolves to a Shop document. + */ +const isShopOwner = async (req, res, next) => { + // TODO: implement + next(); +}; + +/** + * isOrderOwner + * Confirms order.buyer === req.user.id (buyer access) + * OR order.shop.owner === req.user.id (seller access). + */ +const isOrderOwner = async (req, res, next) => { + // TODO: implement + next(); +}; + +/** + * verifyWebhookSignature + * Validates the PayMongo webhook signature header + * using PAYMONGO_WEBHOOK_SECRET from env. + */ +const verifyWebhookSignature = (req, res, next) => { + // TODO: implement + next(); +}; + +module.exports = { + verifyToken, + requireRole, + isShopOwner, + isOrderOwner, + verifyWebhookSignature, +}; diff --git a/modules/auth/.gitkeep b/modules/auth/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/modules/payments/.gitkeep b/modules/payments/.gitkeep deleted file mode 100644 index e69de29..0000000 From a8d382a5bab7d5960823540a22ef4bef0f2c3b2f Mon Sep 17 00:00:00 2001 From: akrs-code Date: Sat, 13 Jun 2026 18:49:47 +0800 Subject: [PATCH 05/37] feat: added newattributes --- modules/orders/order.model.js | 18 +++- modules/products/product.model.js | 13 ++- modules/shops/shop.model.js | 3 + modules/users/user.model.js | 1 + package-lock.json | 161 ++++++++++++++++++++++++++++++ package.json | 1 + 6 files changed, 191 insertions(+), 6 deletions(-) diff --git a/modules/orders/order.model.js b/modules/orders/order.model.js index 3d1c3c0..fe38f6d 100644 --- a/modules/orders/order.model.js +++ b/modules/orders/order.model.js @@ -7,6 +7,9 @@ const orderItemSchema = new mongoose.Schema( name: { type: String, required: true }, // snapshot — safe against product edits price: { type: Number, required: true }, // centavos snapshot quantity: { type: Number, required: true, min: 1 }, + // ── Variant selections (optional — not all products have them) ─ + color: { type: String }, // e.g. 'Natural Ochre' + size: { type: String }, // e.g. 'M / 80×120cm' }, { _id: false } ); @@ -19,10 +22,13 @@ const orderSchema = new mongoose.Schema( // ── Items & Total ───────────────────────────────────────── items: [orderItemSchema], - total: { type: Number, required: true }, // centavos — sum of (price × qty) + total: { type: Number, required: true }, // centavos — sum of (price × qty) + shipping + fees // ── Delivery ────────────────────────────────────────────── deliveryAddress: { type: String, required: true }, + deliveryNotes: { type: String }, // buyer's optional note to courier + shippingFee: { type: Number, default: 0 }, // centavos — locked at time of order + // Optional — seller fills in when handing off to a courier trackingNumber: { type: String }, courier: { type: String }, // e.g. 'LBC', 'J&T', 'Ninja Van', '2GO' @@ -30,14 +36,18 @@ const orderSchema = new mongoose.Schema( // ── Payment ─────────────────────────────────────────────── paymentMethod: { type: String, - enum: ['cod', 'paymongo'], + // 'cod' → Cash on Delivery + // 'card' → Credit/Debit via PayMongo card intent + // 'gcash' → GCash via PayMongo e-wallet + // 'paymongo' → generic PayMongo link (legacy / fallback) + enum: ['cod', 'card', 'gcash', 'paymongo'], default: 'cod', }, - paymentId: { type: String }, // PayMongo payment link ID (null for COD) + paymentId: { type: String }, // PayMongo payment/intent ID (null for COD) // ── Status Flow ─────────────────────────────────────────── // COD: pending → confirmed → shipped → delivered | cancelled - // PayMongo: pending → confirmed (via webhook) → shipped → delivered | cancelled + // Online: pending → confirmed (via webhook) → shipped → delivered | cancelled status: { type: String, enum: ['pending', 'confirmed', 'shipped', 'delivered', 'cancelled'], diff --git a/modules/products/product.model.js b/modules/products/product.model.js index 8bd453c..8229a2f 100644 --- a/modules/products/product.model.js +++ b/modules/products/product.model.js @@ -14,8 +14,17 @@ const productSchema = new mongoose.Schema( // ── Media (S3 key: products/{productId}) ────────────────── imageUrl: { type: String }, - // ── Availability ───────────────────────────────────────── - inStock: { type: Boolean, default: true }, // seller toggles on/off + // ── Variations & Customization ──────────────────────────── + colors: [{ type: String }], + sizes: [{ type: String }], + + // ── Ratings ────────────────────────────────────────────── + rating: { type: Number, default: 0, min: 0, max: 5 }, + reviewCount: { type: Number, default: 0, min: 0 }, + + // ── Availability & Stock ───────────────────────────────── + inStock: { type: Boolean, default: true }, // seller toggles on/off + stockQuantity: { type: Number, default: 0, min: 0 }, }, { timestamps: true, diff --git a/modules/shops/shop.model.js b/modules/shops/shop.model.js index 7a27266..e9ebe9b 100644 --- a/modules/shops/shop.model.js +++ b/modules/shops/shop.model.js @@ -23,6 +23,9 @@ const shopSchema = new mongoose.Schema( // ── Visibility ──────────────────────────────────────────── isActive: { type: Boolean, default: true }, // admin toggles to remove from map + + // ── Aggregated Rating (updated by reviews/orders) ───────── + rating: { type: Number, default: 0, min: 0, max: 5 }, }, { timestamps: true, diff --git a/modules/users/user.model.js b/modules/users/user.model.js index b521a28..6d91263 100644 --- a/modules/users/user.model.js +++ b/modules/users/user.model.js @@ -4,6 +4,7 @@ const userSchema = new mongoose.Schema( { name: { type: String, required: true, trim: true }, email: { type: String, required: true, unique: true, lowercase: true, trim: true }, + phone: { type: String, trim: true }, // e.g. +63 9XX XXX XXXX password: { type: String, required: true, select: false }, // bcrypt hash, hidden by default role: { type: String, enum: ['buyer', 'seller', 'admin'], default: 'buyer' }, avatarUrl: { type: String }, // S3 key: avatars/{userId} diff --git a/package-lock.json b/package-lock.json index 3f68878..b993c60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.1066.0", + "axios": "^1.17.0", "bcrypt": "^6.0.0", "cors": "^2.8.6", "dotenv": "^17.4.2", @@ -622,6 +623,18 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -648,6 +661,24 @@ ], "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -837,6 +868,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -911,6 +954,15 @@ } } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1000,6 +1052,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1129,6 +1196,63 @@ "url": "https://opencollective.com/express" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1255,6 +1379,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -1287,6 +1426,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -1866,6 +2018,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", diff --git a/package.json b/package.json index f45ad9f..a48fc4c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "type": "commonjs", "dependencies": { "@aws-sdk/client-s3": "^3.1066.0", + "axios": "^1.17.0", "bcrypt": "^6.0.0", "cors": "^2.8.6", "dotenv": "^17.4.2", From 045bed1682ab5875ed047c4f3921ed188a5d7d73 Mon Sep 17 00:00:00 2001 From: Yassef Date: Sat, 13 Jun 2026 20:36:10 +0800 Subject: [PATCH 06/37] feat: integrated add Product API --- .env.example | 53 ------------------------ config/db.js | 4 +- modules/orders/order.controller.js | 1 + modules/products/product.controller.js | 56 +++++++++++++++++++++++++- modules/products/product.routes.js | 10 +++++ server.js | 8 ++++ 6 files changed, 76 insertions(+), 56 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 42f97f1..0000000 --- a/.env.example +++ /dev/null @@ -1,53 +0,0 @@ -# ========================= -# SERVER CONFIG -# ========================= -PORT=5000 - -# ========================= -# DATABASE (MongoDB) -# ========================= -MONGODB_URI=mongodb://localhost:27017/your_database_name - -# ========================= -# JWT AUTH -# ========================= -JWT_SECRET=your_jwt_secret_key_here -JWT_EXPIRES_IN=7d - -# ========================= -# AWS S3 (File Uploads) -# ========================= -AWS_REGION=ap-southeast-1 -AWS_ACCESS_KEY_ID=your_aws_access_key -AWS_SECRET_ACCESS_KEY=your_aws_secret_key -AWS_S3_BUCKET_NAME=your_bucket_name - -# ========================= -# STRIPE (Payments - Global) -# ========================= -STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key -STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret - -# ========================= -# PAYMONGO (Philippines Payments) -# ========================= -PAYMONGO_SECRET_KEY=sk_test_your_paymongo_secret_key -PAYMONGO_PUBLIC_KEY=pk_test_your_paymongo_public_key - -# ========================= -# FRONTEND URL (CORS) -# ========================= -CLIENT_URL=http://localhost:3000 - -# ========================= -# OPTIONAL (Email service) -# ========================= -EMAIL_HOST=smtp.gmail.com -EMAIL_PORT=587 -EMAIL_USER=your_email@gmail.com -EMAIL_PASS=your_email_password - -# ========================= -# LOGGING -# ========================= -LOG_LEVEL=debug \ No newline at end of file diff --git a/config/db.js b/config/db.js index 91826b0..ea21b08 100644 --- a/config/db.js +++ b/config/db.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const connectDB = async () => { try { - const mongoURI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/artisan_hub'; + const mongoURI = process.env.MONGO_URI || 'mongodb://localhost:27017/artisan_hub'; await mongoose.connect(mongoURI); console.log('✅ MongoDB Connected'); } catch (err) { @@ -11,4 +11,4 @@ const connectDB = async () => { } }; -module.exports = connectDB; +module.exports = connectDB; \ No newline at end of file diff --git a/modules/orders/order.controller.js b/modules/orders/order.controller.js index 3455ed7..6e67cc1 100644 --- a/modules/orders/order.controller.js +++ b/modules/orders/order.controller.js @@ -8,4 +8,5 @@ // deliverOrder — seller: shipped → delivered (marks as complete) // cancelOrder — buyer or seller: any pre-shipped status → cancelled + module.exports = {}; diff --git a/modules/products/product.controller.js b/modules/products/product.controller.js index 106bff9..68bceb3 100644 --- a/modules/products/product.controller.js +++ b/modules/products/product.controller.js @@ -7,4 +7,58 @@ // deleteProduct — seller / admin: remove product // moderateProduct — admin: approve or reject product listing -module.exports = {}; +const Product = require('./product.model'); + + +// createProduct +const createProduct = async (req, res, next) => { + try { + const { shopId } = req.params; + + const { + name, + description, + price, + category, + imageUrl, + colors, + sizes, + stockQuantity + } = req.body; + + // Validation + if (!name || price === undefined) { + return res.status(400).json({ + status: 'error', + message: 'Name and price are required' + }); + } + + const product = await Product.create({ + shop: shopId, + name, + description, + price, + category, + imageUrl, + colors, + sizes, + stockQuantity, + inStock: stockQuantity > 0 + }); + + res.status(201).json({ + status: 'success', + message: 'Product created successfully', + data: product + }); + + } catch (error) { + next(error); + } +}; + +module.exports = { + createProduct +}; + diff --git a/modules/products/product.routes.js b/modules/products/product.routes.js index eabdb91..bb4e7f6 100644 --- a/modules/products/product.routes.js +++ b/modules/products/product.routes.js @@ -11,4 +11,14 @@ const router = express.Router(); // DELETE /api/products/:id — seller (isShopOwner): remove product // POST /api/products/:id/upload — seller: get pre-signed S3 URL for product image + +const { + createProduct +} = require('./product.controller'); + +/* +POST /api/shops/:shopId/products +*/ +router.post('/shops/:shopId/products', createProduct); + module.exports = router; diff --git a/server.js b/server.js index e3c221b..75f08a4 100644 --- a/server.js +++ b/server.js @@ -29,6 +29,14 @@ app.get('/api/health', (req, res) => { // app.use('/api/payments', require('./modules/payments/routes')); app.use('/api/users', require('./modules/users/user.routes')); + + + +// POST /api/shops/:shopId/products +app.use('/api', require('./modules/products/product.routes')); + + + // Global Error Handler app.use((err, req, res, next) => { console.error(err.stack); From 318e46e75d3b40eb8658ea12346c4245655a66a2 Mon Sep 17 00:00:00 2001 From: Yassef Date: Sat, 13 Jun 2026 22:02:21 +0800 Subject: [PATCH 07/37] feat: integrated register user returning the whole user payload, status ,message and token --- modules/auth/auth.controller.js | 78 ++++++++++++++++++++++++++++++++- modules/auth/auth.routes.js | 11 ++++- server.js | 22 +++++++++- 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/modules/auth/auth.controller.js b/modules/auth/auth.controller.js index 94f3f3e..b3f4611 100644 --- a/modules/auth/auth.controller.js +++ b/modules/auth/auth.controller.js @@ -1,7 +1,81 @@ // TODO: Implement auth controller // Planned handlers: -// register — hash password (bcrypt), create user with role, return JWT +// register — hash password (bcrypt), create user with role, return JWT (okay na to) // login — compare hash, check isActive, return JWT + user object // getMe — decode token, return current user (used to restore session on app load) -module.exports = {}; +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +const User = require('../users/user.model'); + + +// register the user +const register = async (req, res, next) => { + try { + const { + name, + email, + phone, + password, + role + } = req.body; + + // Validation + if (!name || !email || !password) { + return res.status(400).json({ + status: 'error', + message: 'Name, email, and password are required' + }); + } + + // Check existing user (bawal ang dalawa yung email ofc) + const existingUser = await User.findOne({ email }); + + if (existingUser) { + return res.status(400).json({ + status: 'error', + message: 'Email already registered' + }); + } + + // Hash password plus yung bcrypt + const hashedPassword = await bcrypt.hash(password, 10); + + // Create user + const user = await User.create({ + name, + email, + phone, + password: hashedPassword, + role: role || 'buyer' + }); + + // Generate ng JWtoken Mag expire ng 7days + const token = jwt.sign( + { + id: user._id, + role: user.role + }, + process.env.JWT_SECRET, + { + expiresIn: process.env.JWT_EXPIRES_IN + } + ); + + res.status(201).json({ + status: 'success', + message: 'Registration successful', + token, + user + }); + + } catch (error) { + next(error); + } +}; + + +module.exports = { + register, + login +}; diff --git a/modules/auth/auth.routes.js b/modules/auth/auth.routes.js index 960a31c..055e021 100644 --- a/modules/auth/auth.routes.js +++ b/modules/auth/auth.routes.js @@ -2,8 +2,17 @@ const express = require('express'); const router = express.Router(); -// POST /api/auth/register — create account, assign role, return JWT +// POST /api/auth/register — create account, assign role, return JWT (okay na to) // POST /api/auth/login — verify credentials, return JWT + user object // GET /api/auth/me — protected: return current user from token (session restore) + +const { + register +} = require('./auth.controller'); + +// POST /api/auth/register +router.post('/register', register); + + module.exports = router; diff --git a/server.js b/server.js index 75f08a4..74297fd 100644 --- a/server.js +++ b/server.js @@ -30,10 +30,30 @@ app.get('/api/health', (req, res) => { app.use('/api/users', require('./modules/users/user.routes')); +// ALL About Authentication +app.use('/api/auth', require('./modules/auth/auth.routes')); +// Example payload { +// "name": "Juan Dela Cruz", +// "email": "juan@example.com", +// "phone": "09123456789", +// "password": "password123", +// "role": "seller" +// } - +// All About Product // POST /api/shops/:shopId/products app.use('/api', require('./modules/products/product.routes')); +// Example na payload is { +// "name": "Handmade Bag", +// "description": "Locally crafted handbag.", +// "price": 2500, +// "category": "Accessories", +// "imageUrl": "https://example.com/bag.jpg", +// "colors": ["Brown", "Black"], +// "sizes": ["Medium"], +// "stockQuantity": 10 +// } + From c6b6ce4048bf9e1a504ac17063b389c02df2ed0b Mon Sep 17 00:00:00 2001 From: Yassef Date: Sat, 13 Jun 2026 23:42:39 +0800 Subject: [PATCH 08/37] feat: integrated a login that returns token and object --- middleware/auth.js | 152 +++++++++++++++++++++++++++--- modules/auth/auth.controller.js | 73 ++++++++++++++ modules/auth/auth.routes.js | 19 +++- modules/products/product.model.js | 1 + server.js | 8 +- 5 files changed, 233 insertions(+), 20 deletions(-) diff --git a/middleware/auth.js b/middleware/auth.js index 23f64de..70ef36e 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,7 +1,8 @@ // TODO: Implement auth middleware const jwt = require('jsonwebtoken'); - +const Shop = require('../shops/shop.model'); // adjust path if needed +const Order = require('../orders/order.model'); // adjust if exists /** * verifyToken * Checks Authorization: Bearer , decodes it, @@ -9,8 +10,33 @@ const jwt = require('jsonwebtoken'); * Usage: router.get('/protected', verifyToken, handler) */ const verifyToken = (req, res, next) => { - // TODO: implement - next(); + try { + const authHeader = req.headers.authorization; + + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + status: 'error', + message: 'No token provided' + }); + } + + const token = authHeader.split(' ')[1]; + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + req.user = { + id: decoded.id, + role: decoded.role + }; + + next(); + + } catch (error) { + return res.status(401).json({ + status: 'error', + message: 'Invalid or expired token' + }); + } }; /** @@ -18,9 +44,24 @@ const verifyToken = (req, res, next) => { * Factory — checks req.user.role against the allowed list. * Usage: router.get('/admin', verifyToken, requireRole('admin'), handler) */ -const requireRole = (...roles) => (req, res, next) => { - // TODO: implement - next(); +const requireRole = (...roles) => { + return (req, res, next) => { + if (!req.user) { + return res.status(401).json({ + status: 'error', + message: 'Unauthorized' + }); + } + + if (!roles.includes(req.user.role)) { + return res.status(403).json({ + status: 'error', + message: 'Forbidden: insufficient permissions' + }); + } + + next(); + }; }; /** @@ -29,8 +70,38 @@ const requireRole = (...roles) => (req, res, next) => { * Expects :shopId or :id param that resolves to a Shop document. */ const isShopOwner = async (req, res, next) => { - // TODO: implement - next(); + try { + const shopId = req.params.shopId || req.params.id; + + if (!shopId) { + return res.status(400).json({ + status: 'error', + message: 'Shop ID is required' + }); + } + + const shop = await Shop.findById(shopId); + + if (!shop) { + return res.status(404).json({ + status: 'error', + message: 'Shop not found' + }); + } + + if (shop.owner.toString() !== req.user.id) { + return res.status(403).json({ + status: 'error', + message: 'You are not the owner of this shop' + }); + } + + req.shop = shop; + next(); + + } catch (error) { + next(error); + } }; /** @@ -39,18 +110,75 @@ const isShopOwner = async (req, res, next) => { * OR order.shop.owner === req.user.id (seller access). */ const isOrderOwner = async (req, res, next) => { - // TODO: implement - next(); + try { + const orderId = req.params.id; + + const order = await Order.findById(orderId).populate('shop'); + + if (!order) { + return res.status(404).json({ + status: 'error', + message: 'Order not found' + }); + } + + const isBuyer = order.buyer.toString() === req.user.id; + const isSeller = order.shop.owner.toString() === req.user.id; + + if (!isBuyer && !isSeller) { + return res.status(403).json({ + status: 'error', + message: 'Access denied' + }); + } + + req.order = order; + next(); + + } catch (error) { + next(error); + } }; + /** * verifyWebhookSignature * Validates the PayMongo webhook signature header * using PAYMONGO_WEBHOOK_SECRET from env. */ const verifyWebhookSignature = (req, res, next) => { - // TODO: implement - next(); + try { + const signature = req.headers['paymongo-signature']; + + if (!signature) { + return res.status(401).json({ + status: 'error', + message: 'Missing webhook signature' + }); + } + + const expected = process.env.PAYMONGO_WEBHOOK_SECRET; + + if (!expected) { + return res.status(500).json({ + status: 'error', + message: 'Webhook secret not configured' + }); + } + + // Simple validation (depends on PayMongo format) + if (signature !== expected) { + return res.status(401).json({ + status: 'error', + message: 'Invalid webhook signature' + }); + } + + next(); + + } catch (error) { + next(error); + } }; module.exports = { diff --git a/modules/auth/auth.controller.js b/modules/auth/auth.controller.js index b3f4611..f451446 100644 --- a/modules/auth/auth.controller.js +++ b/modules/auth/auth.controller.js @@ -74,7 +74,80 @@ const register = async (req, res, next) => { } }; +// const login = async (req , res, next)=>{ +// const {username , password} = req.body; +// if(!username || !password){ +// return res.status(400).json({ +// status : "error", +// message : "Username and password is required" +// }); +// } + +// }; +const login = async (req, res, next) => { + try { + const { email, password } = req.body; + + // Validation + if (!email || !password) { + return res.status(400).json({ + status: 'error', + message: 'Email and password are required' + }); + } + + // Find user + const user = await User.findOne({ email }).select('+password'); + + if (!user) { + return res.status(400).json({ + status: 'error', + message: 'Invalid email or password' + }); + } + + // Check if user is active + if (!user.isActive) { + return res.status(403).json({ + status: 'error', + message: 'Account is disabled' + }); + } + + // Compare password or verify nalang sana kaso naka native yung bcrypt + const isMatch = await bcrypt.compare(password, user.password); + + if (!isMatch) { + return res.status(400).json({ + status: 'error', + message: 'Invalid email or password' + }); + } + + // Create token + const token = jwt.sign( + { + id: user._id, + role: user.role + }, + process.env.JWT_SECRET, + { + expiresIn: process.env.JWT_EXPIRES_IN || '7d' + } + ); + + res.status(200).json({ + status: 'success', + message: 'Login successful', + token, + user + }); + + } catch (error) { + next(error); + } +}; module.exports = { register, login diff --git a/modules/auth/auth.routes.js b/modules/auth/auth.routes.js index 055e021..2c9dfca 100644 --- a/modules/auth/auth.routes.js +++ b/modules/auth/auth.routes.js @@ -8,11 +8,28 @@ const router = express.Router(); const { - register + register, + login } = require('./auth.controller'); // POST /api/auth/register router.post('/register', register); +// Example payload { +// "name": "Juan Dela Cruz", +// "email": "juan@example.com", +// "phone": "09123456789", +// "password": "password123", +// "role": "seller" +// } + + + +router.post('/login', login); +// Example Payload{ +// "email": "juan@example.com", +// "password": "password123" +// } + module.exports = router; diff --git a/modules/products/product.model.js b/modules/products/product.model.js index 8229a2f..fec4cb4 100644 --- a/modules/products/product.model.js +++ b/modules/products/product.model.js @@ -21,6 +21,7 @@ const productSchema = new mongoose.Schema( // ── Ratings ────────────────────────────────────────────── rating: { type: Number, default: 0, min: 0, max: 5 }, reviewCount: { type: Number, default: 0, min: 0 }, + review: { type: String }, // ── Availability & Stock ───────────────────────────────── inStock: { type: Boolean, default: true }, // seller toggles on/off diff --git a/server.js b/server.js index 74297fd..df021fa 100644 --- a/server.js +++ b/server.js @@ -32,13 +32,7 @@ app.use('/api/users', require('./modules/users/user.routes')); // ALL About Authentication app.use('/api/auth', require('./modules/auth/auth.routes')); -// Example payload { -// "name": "Juan Dela Cruz", -// "email": "juan@example.com", -// "phone": "09123456789", -// "password": "password123", -// "role": "seller" -// } + // All About Product // POST /api/shops/:shopId/products From 5958728254d82d73fe506b91cd220ec42393f5f2 Mon Sep 17 00:00:00 2001 From: Yassef Date: Mon, 15 Jun 2026 02:31:07 +0800 Subject: [PATCH 09/37] feat: added a Third party Storage API (Cloudinary) --- middleware/cloudinary.js | 9 +++++++++ modules/products/product.model.js | 2 +- services/uploader.js | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 middleware/cloudinary.js create mode 100644 services/uploader.js diff --git a/middleware/cloudinary.js b/middleware/cloudinary.js new file mode 100644 index 0000000..876c89e --- /dev/null +++ b/middleware/cloudinary.js @@ -0,0 +1,9 @@ +const cloudinary = require('cloudinary').v2; + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}); + +module.exports = cloudinary; \ No newline at end of file diff --git a/modules/products/product.model.js b/modules/products/product.model.js index fec4cb4..8681879 100644 --- a/modules/products/product.model.js +++ b/modules/products/product.model.js @@ -11,7 +11,7 @@ const productSchema = new mongoose.Schema( price: { type: Number, required: true, min: 0 }, // stored in centavos (integer) category: { type: String }, // optional tag - // ── Media (S3 key: products/{productId}) ────────────────── + imageUrl: { type: String }, // ── Variations & Customization ──────────────────────────── diff --git a/services/uploader.js b/services/uploader.js new file mode 100644 index 0000000..30672cf --- /dev/null +++ b/services/uploader.js @@ -0,0 +1,17 @@ +const cloudinary = require('../config/cloudinary'); + +const uploadImage = async (image, folder = 'artisan-hub') => { + try { + const result = await cloudinary.uploader.upload(image, { + folder, + }); + + return result; + } catch (error) { + throw error; + } +}; + +module.exports = { + uploadImage, +}; \ No newline at end of file From 537b0cf60995f08bf255c6d6fbbad6e2a1c5f5e2 Mon Sep 17 00:00:00 2001 From: Yassef Date: Mon, 15 Jun 2026 03:44:02 +0800 Subject: [PATCH 10/37] chore: Changed the way how uploader in mutler and cloudinary is being handled feat: Upload image in product chore: added multer and cloudinary modules --- {middleware => config}/cloudinary.js | 0 middleware/multer.js | 14 +++ modules/products/product.controller.js | 17 ++- modules/products/product.routes.js | 16 ++- package-lock.json | 163 +++++++++++++++++++++++++ package.json | 2 + services/uploader.js | 18 +-- 7 files changed, 210 insertions(+), 20 deletions(-) rename {middleware => config}/cloudinary.js (100%) create mode 100644 middleware/multer.js diff --git a/middleware/cloudinary.js b/config/cloudinary.js similarity index 100% rename from middleware/cloudinary.js rename to config/cloudinary.js diff --git a/middleware/multer.js b/middleware/multer.js new file mode 100644 index 0000000..020ec9d --- /dev/null +++ b/middleware/multer.js @@ -0,0 +1,14 @@ +const multer = require('multer'); + +const storage = multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, 'uploads/'); + }, + filename: function (req, file, cb) { + cb(null, Date.now() + '-' + file.originalname); + } +}); + +const upload = multer({ storage }); + +module.exports = upload; \ No newline at end of file diff --git a/modules/products/product.controller.js b/modules/products/product.controller.js index 68bceb3..3ea432f 100644 --- a/modules/products/product.controller.js +++ b/modules/products/product.controller.js @@ -8,9 +8,8 @@ // moderateProduct — admin: approve or reject product listing const Product = require('./product.model'); +const { uploadImage } = require('../../services/uploader'); - -// createProduct const createProduct = async (req, res, next) => { try { const { shopId } = req.params; @@ -20,13 +19,11 @@ const createProduct = async (req, res, next) => { description, price, category, - imageUrl, colors, sizes, stockQuantity } = req.body; - // Validation if (!name || price === undefined) { return res.status(400).json({ status: 'error', @@ -34,6 +31,14 @@ const createProduct = async (req, res, next) => { }); } + let imageUrl = ''; + + // catch by multer muna + if (req.file) { + const uploadedImage = await uploadImage(req.file.path, 'products'); + imageUrl = uploadedImage.secure_url; + } + const product = await Product.create({ shop: shopId, name, @@ -43,8 +48,8 @@ const createProduct = async (req, res, next) => { imageUrl, colors, sizes, - stockQuantity, - inStock: stockQuantity > 0 + stockQuantity: stockQuantity || 0, + inStock: (stockQuantity || 0) > 0 }); res.status(201).json({ diff --git a/modules/products/product.routes.js b/modules/products/product.routes.js index bb4e7f6..b489f05 100644 --- a/modules/products/product.routes.js +++ b/modules/products/product.routes.js @@ -1,7 +1,7 @@ // TODO: Implement products routes const express = require('express'); const router = express.Router(); - +const upload = require('../../middleware/multer'); // Middleware: verifyToken, requireRole('seller'), isShopOwner // GET /api/shops/:shopId/products — public: list products for a shop @@ -19,6 +19,18 @@ const { /* POST /api/shops/:shopId/products */ -router.post('/shops/:shopId/products', createProduct); + + +router.post('/shops/:shopId/products', upload.single('image'), createProduct); +// Example payload { +// "name": "Wooden Chair", +// "description": "Handmade premium wooden chair made from solid mahogany wood", +// "price": 2500, +// "category": "Furniture", +// "imageUrl": "https://res.cloudinary.com/demo/image/upload/sample.jpg", (hindi gagana if raw json need mo ng file handler) +// "colors": ["Brown", "Dark Brown"], +// "sizes": ["Small", "Medium"], +// "stockQuantity": 10 +// } module.exports = router; diff --git a/package-lock.json b/package-lock.json index b993c60..72e0ea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,14 @@ "@aws-sdk/client-s3": "^3.1066.0", "axios": "^1.17.0", "bcrypt": "^6.0.0", + "cloudinary": "^2.10.0", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mongoose": "^9.7.0", "morgan": "^1.11.0", + "multer": "^2.1.1", "stripe": "^22.2.0" }, "devDependencies": { @@ -661,6 +663,12 @@ ], "license": "MIT" }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -805,6 +813,23 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -868,6 +893,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/cloudinary": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.10.0.tgz", + "integrity": "sha512-sY09kYg7wprkndAOjZBAYqFZqwL+SxnEGcAvksOvFA+5upnFn949UjkEkHKNSwkBtW/xRDd0p6NgbSXZcxkI3w==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.23" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -880,6 +917,21 @@ "node": ">= 0.8" } }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -1581,6 +1633,12 @@ "node": ">=18.0.0" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -1839,6 +1897,68 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/multer/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/multer/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -2082,6 +2202,20 @@ "node": ">= 0.10" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2309,6 +2443,23 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/stripe": { "version": "22.2.0", "resolved": "https://registry.npmjs.org/stripe/-/stripe-22.2.0.tgz", @@ -2435,6 +2586,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2451,6 +2608,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index a48fc4c..887fcd3 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,14 @@ "@aws-sdk/client-s3": "^3.1066.0", "axios": "^1.17.0", "bcrypt": "^6.0.0", + "cloudinary": "^2.10.0", "cors": "^2.8.6", "dotenv": "^17.4.2", "express": "^5.2.1", "jsonwebtoken": "^9.0.3", "mongoose": "^9.7.0", "morgan": "^1.11.0", + "multer": "^2.1.1", "stripe": "^22.2.0" }, "devDependencies": { diff --git a/services/uploader.js b/services/uploader.js index 30672cf..c6013c7 100644 --- a/services/uploader.js +++ b/services/uploader.js @@ -1,17 +1,11 @@ const cloudinary = require('../config/cloudinary'); -const uploadImage = async (image, folder = 'artisan-hub') => { - try { - const result = await cloudinary.uploader.upload(image, { - folder, - }); +const uploadImage = async (filePath, folder = 'artisan-hub') => { + const result = await cloudinary.uploader.upload(filePath, { + folder, + }); - return result; - } catch (error) { - throw error; - } + return result; }; -module.exports = { - uploadImage, -}; \ No newline at end of file +module.exports = { uploadImage }; \ No newline at end of file From 5708af97717a9ba1bc669fb2fe69486b5568351b Mon Sep 17 00:00:00 2001 From: Yassef Date: Mon, 15 Jun 2026 04:05:07 +0800 Subject: [PATCH 11/37] chore: updated/ improved the register user allowing to send an image --- modules/auth/auth.controller.js | 9 +++++++++ modules/auth/auth.routes.js | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/modules/auth/auth.controller.js b/modules/auth/auth.controller.js index f451446..57e131a 100644 --- a/modules/auth/auth.controller.js +++ b/modules/auth/auth.controller.js @@ -7,6 +7,7 @@ const bcrypt = require('bcrypt'); const jwt = require('jsonwebtoken'); const User = require('../users/user.model'); +const { uploadImage } = require('../../services/uploader'); // register the user @@ -28,6 +29,13 @@ const register = async (req, res, next) => { }); } + let avatarUrl = ''; + + if (req.file) { + const uploadedImage = await uploadImage(req.file.path, 'users'); + avatarUrl = uploadedImage.secure_url; + } + // Check existing user (bawal ang dalawa yung email ofc) const existingUser = await User.findOne({ email }); @@ -47,6 +55,7 @@ const register = async (req, res, next) => { email, phone, password: hashedPassword, + avatarUrl, role: role || 'buyer' }); diff --git a/modules/auth/auth.routes.js b/modules/auth/auth.routes.js index 2c9dfca..b91b84c 100644 --- a/modules/auth/auth.routes.js +++ b/modules/auth/auth.routes.js @@ -1,6 +1,7 @@ // TODO: Implement auth routes const express = require('express'); const router = express.Router(); +const upload = require('../../middleware/multer'); // POST /api/auth/register — create account, assign role, return JWT (okay na to) // POST /api/auth/login — verify credentials, return JWT + user object @@ -13,7 +14,7 @@ const { } = require('./auth.controller'); // POST /api/auth/register -router.post('/register', register); +router.post('/register',upload.single('image'), register); // Example payload { // "name": "Juan Dela Cruz", // "email": "juan@example.com", From f73a570e71e45793be4b76bb47bdb617efeebe3c Mon Sep 17 00:00:00 2001 From: Yassef Date: Mon, 15 Jun 2026 04:47:48 +0800 Subject: [PATCH 12/37] feat: Session restore task getMe --- middleware/auth.js | 4 ++-- modules/auth/auth.controller.js | 25 ++++++++++++++++++++++++- modules/auth/auth.routes.js | 12 +++++++++--- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/middleware/auth.js b/middleware/auth.js index 70ef36e..f231b6b 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,8 +1,8 @@ // TODO: Implement auth middleware const jwt = require('jsonwebtoken'); -const Shop = require('../shops/shop.model'); // adjust path if needed -const Order = require('../orders/order.model'); // adjust if exists +const Shop = require('../modules/shops/shop.model'); // adjust path if needed +const Order = require('../modules/orders/order.model'); // adjust if exists /** * verifyToken * Checks Authorization: Bearer , decodes it, diff --git a/modules/auth/auth.controller.js b/modules/auth/auth.controller.js index 57e131a..949674d 100644 --- a/modules/auth/auth.controller.js +++ b/modules/auth/auth.controller.js @@ -157,7 +157,30 @@ const login = async (req, res, next) => { next(error); } }; + + +const getMe = async (req, res, next) => { + try { + const user = await User.findById(req.user.id); + + if (!user) { + return res.status(404).json({ + status: 'error', + message: 'User not found' + }); + } + + res.status(200).json({ + status: 'success', + user + }); + + } catch (error) { + next(error); + } +}; module.exports = { register, - login + login, + getMe }; diff --git a/modules/auth/auth.routes.js b/modules/auth/auth.routes.js index b91b84c..1b2b3de 100644 --- a/modules/auth/auth.routes.js +++ b/modules/auth/auth.routes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const upload = require('../../middleware/multer'); - +const { verifyToken } = require('../../middleware/auth') // POST /api/auth/register — create account, assign role, return JWT (okay na to) // POST /api/auth/login — verify credentials, return JWT + user object // GET /api/auth/me — protected: return current user from token (session restore) @@ -10,7 +10,8 @@ const upload = require('../../middleware/multer'); const { register, - login + login, + getMe } = require('./auth.controller'); // POST /api/auth/register @@ -18,6 +19,7 @@ router.post('/register',upload.single('image'), register); // Example payload { // "name": "Juan Dela Cruz", // "email": "juan@example.com", +// "avatarUrl" : // "phone": "09123456789", // "password": "password123", // "role": "seller" @@ -26,11 +28,15 @@ router.post('/register',upload.single('image'), register); - router.post('/login', login); // Example Payload{ // "email": "juan@example.com", // "password": "password123" // } + +// POST /api/auth/login +router.get('/me',verifyToken, getMe); + + module.exports = router; From 15ac26e357514e077f36af5f64c2e05358561e42 Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 10:41:06 +0800 Subject: [PATCH 13/37] feat: get user profile --- modules/products/product.routes.js | 2 + modules/users/user.controller.js | 75 +++++++++++++++++++++++++++++- modules/users/user.routes.js | 14 +++++- server.js | 17 ++----- 4 files changed, 93 insertions(+), 15 deletions(-) diff --git a/modules/products/product.routes.js b/modules/products/product.routes.js index b489f05..7576ab7 100644 --- a/modules/products/product.routes.js +++ b/modules/products/product.routes.js @@ -33,4 +33,6 @@ router.post('/shops/:shopId/products', upload.single('image'), createProduct); // "sizes": ["Small", "Medium"], // "stockQuantity": 10 // } + + module.exports = router; diff --git a/modules/users/user.controller.js b/modules/users/user.controller.js index 0d3d4f3..72bc7fc 100644 --- a/modules/users/user.controller.js +++ b/modules/users/user.controller.js @@ -1,7 +1,80 @@ + + +const User = require('./user.model'); +const {uploadImage } = require('../../services/uploader'); + // TODO: Implement users controller // Planned handlers: // getMyProfile — return current user's profile (name, email, avatarUrl, role) // updateMyProfile — update own name, avatarUrl // getUploadUrl — return pre-signed S3 URL for avatar upload -module.exports = {}; + +const getProfile = async (req, res, next) => { + try { + const user = await User.findById(req.user.id); + + if (!user) { + return res.status(404).json({ + status: 'error', + message: 'User not found' + }); + } + + res.status(200).json({ + status: 'success', + data: user + }); + + } catch (error) { + next(error); + } +}; + +const updateProfile = async (req, res, next) => { + try { + const { name } = req.body; + + const updates = {}; + + + if (name) { + updates.name = name; + } + + // Upload new avatar if file exists + if (req.file) { + const uploadedImage = await uploadImage(req.file.path, 'users'); + updates.avatarUrl = uploadedImage.secure_url; + } + + const user = await User.findByIdAndUpdate( + req.user.id, + updates, + { + new: true, + runValidators: true, + } + ); + + if (!user) { + return res.status(404).json({ + status: 'error', + message: 'User not found', + }); + } + + res.status(200).json({ + status: 'success', + message: 'Profile updated successfully', + data: user, + }); + + } catch (error) { + next(error); + } +}; +module.exports = { +getProfile, +updateProfile, +}; diff --git a/modules/users/user.routes.js b/modules/users/user.routes.js index 9a68adc..09343ca 100644 --- a/modules/users/user.routes.js +++ b/modules/users/user.routes.js @@ -1,11 +1,23 @@ // TODO: Implement user routes const express = require('express'); const router = express.Router(); +const { getProfile } = require('./user.controller'); +const { updateProfile } = require('./user.controller'); +const { verifyToken } = require('../../middleware/auth'); +const upload = require('../../middleware/multer'); -// All routes require verifyToken middleware + +// All routes require verifyToken middleware // GET /api/users/profile — get own profile // PUT /api/users/profile — update own name, avatarUrl // POST /api/users/profile/upload — get pre-signed S3 URL for avatar upload +/* +GET /api/users/profile +*/ +router.get('/profile', verifyToken, getProfile); + + + module.exports = router; diff --git a/server.js b/server.js index df021fa..2b57f50 100644 --- a/server.js +++ b/server.js @@ -30,23 +30,14 @@ app.get('/api/health', (req, res) => { app.use('/api/users', require('./modules/users/user.routes')); -// ALL About Authentication + app.use('/api/auth', require('./modules/auth/auth.routes')); -// All About Product -// POST /api/shops/:shopId/products + + app.use('/api', require('./modules/products/product.routes')); -// Example na payload is { -// "name": "Handmade Bag", -// "description": "Locally crafted handbag.", -// "price": 2500, -// "category": "Accessories", -// "imageUrl": "https://example.com/bag.jpg", -// "colors": ["Brown", "Black"], -// "sizes": ["Medium"], -// "stockQuantity": 10 -// } + From 237f11ba759c5a185f1d777e644b495f11c1f918 Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 10:48:50 +0800 Subject: [PATCH 14/37] feat: user profile update --- modules/users/user.routes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/users/user.routes.js b/modules/users/user.routes.js index 09343ca..f953d1f 100644 --- a/modules/users/user.routes.js +++ b/modules/users/user.routes.js @@ -18,6 +18,7 @@ GET /api/users/profile */ router.get('/profile', verifyToken, getProfile); - +// PUT /api/users/profile (for update) +router.put('/profile', verifyToken, upload.single('image'), updateProfile); module.exports = router; From 06ad4c0e74e1eed712b780cfc578f7b13e783d33 Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 11:16:09 +0800 Subject: [PATCH 15/37] feat: made a getnearby shop and shop by id --- modules/shops/shop.controller.js | 106 ++++++++++++++++++++++++++++++- modules/shops/shop.routes.js | 15 ++++- modules/users/user.routes.js | 4 +- server.js | 3 + 4 files changed, 123 insertions(+), 5 deletions(-) diff --git a/modules/shops/shop.controller.js b/modules/shops/shop.controller.js index 9cfaf0a..26ca798 100644 --- a/modules/shops/shop.controller.js +++ b/modules/shops/shop.controller.js @@ -1,3 +1,5 @@ +const Shop = require('./shop.model') + // TODO: Implement shops controller // Planned handlers: // @@ -13,4 +15,106 @@ // getReviews — public: list seeded reviews for a shop // createReview — buyer: submit a 1–5 star review with optional comment -module.exports = {}; + +const getShops = async (req, res, next) => { + try { + const { category } = req.query; + + // Base filter + const filter = { + isActive: true + }; + + // Add category filter if provided + if (category) { + filter.category = category; + } + + const shops = await Shop.find(filter) + .populate('owner', 'name email') + .sort({ createdAt: -1 }); + + res.status(200).json({ + status: 'success', + count: shops.length, + data: shops + }); + + } catch (error) { + next(error); + } +}; +// getShopsNearby — geo-query: shops near [lng, lat] within radius ($near + 2dsphere) +const getNearbyShops = async (req, res, next) => { + try { + const { lat, lng, radius } = req.query; + + // Validation + if (!lat || !lng) { + return res.status(400).json({ + status: 'error', + message: 'Latitude and longitude are required' + }); + } + + const shops = await Shop.find({ + isActive: true, + location: { + $near: { + $geometry: { + type: 'Point', + coordinates: [ + parseFloat(lng), // longitude first + parseFloat(lat) // latitude second + ] + }, + $maxDistance: radius + ? parseInt(radius) + : 5000 // default: 5 km (change mo lang kung ano default) + } + } + }); + + res.status(200).json({ + status: 'success', + count: shops.length, + data: shops + }); + + } catch (error) { + next(error); + } +}; + +// getShopById — single shop detail (includes product list) +const getShopById = async (req, res, next) => { + try { + const { id } = req.params; + + const shop = await Shop.findById(id) + .populate('owner', 'name email phone avatarUrl'); + + if (!shop) { + return res.status(404).json({ + status: 'error', + message: 'Shop not found' + }); + } + + res.status(200).json({ + status: 'success', + data: shop + }); + + } catch (error) { + next(error); + } +}; + + + +module.exports = { + getShops, + getNearbyShops, + getShopById, +}; diff --git a/modules/shops/shop.routes.js b/modules/shops/shop.routes.js index 86bc728..baa9a5d 100644 --- a/modules/shops/shop.routes.js +++ b/modules/shops/shop.routes.js @@ -1,11 +1,14 @@ -// TODO: Implement shops routes + const express = require('express'); const router = express.Router(); +const { isShopOwner,verifyToken,requireRole } = require('../../middleware/auth'); +const { getShops, getNearbyShops,getShopById } = require('./shop.controller'); +// TODO: Implement shops routes // Middleware: verifyToken, requireRole('seller'), isShopOwner // ── Shop routes ──────────────────────────────────────────────────────────── -// GET /api/shops — public: list shops (filter by category, isActive: true) +// GET /api/shops — public: list shops (filter by category, isActive: true) / // GET /api/shops/nearby — public: geo-query ?lat=&lng=&radius= (uses 2dsphere $near) // GET /api/shops/:id — public: shop detail + product list // POST /api/shops — seller: create shop with coords → pin appears on map @@ -16,4 +19,12 @@ const router = express.Router(); // GET /api/shops/:id/reviews — public: list reviews for a shop (seeded for demo) // POST /api/shops/:id/reviews — buyer (verifyToken): submit a review +// Public: List active shops (or add this query to filter category, isActive) +router.get('/', getShops); + +router.get('/nearby', getNearbyShops); + +router.get('/:id', getShopById); + + module.exports = router; diff --git a/modules/users/user.routes.js b/modules/users/user.routes.js index f953d1f..57bc662 100644 --- a/modules/users/user.routes.js +++ b/modules/users/user.routes.js @@ -9,8 +9,8 @@ const upload = require('../../middleware/multer'); // All routes require verifyToken middleware -// GET /api/users/profile — get own profile -// PUT /api/users/profile — update own name, avatarUrl +// GET /api/users/profile — get own profile / +// PUT /api/users/profile — update own name, avatarUrl / // POST /api/users/profile/upload — get pre-signed S3 URL for avatar upload /* diff --git a/server.js b/server.js index 2b57f50..4a94399 100644 --- a/server.js +++ b/server.js @@ -27,6 +27,9 @@ app.get('/api/health', (req, res) => { // TODO: Import and use your module routes here // app.use('/api/auth', require('./modules/auth/routes')); // app.use('/api/payments', require('./modules/payments/routes')); + +app.use('/api/shops', require('./modules/shops/shop.routes')); + app.use('/api/users', require('./modules/users/user.routes')); From 6b092966144365482200585c5bc627dc2556905f Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 11:27:58 +0800 Subject: [PATCH 16/37] =?UTF-8?q?feat:seller:=20create=20shop=20with=20coo?= =?UTF-8?q?rds=20=E2=86=92=20pin=20appears=20on=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/shops/shop.controller.js | 45 ++++++++++++++++++++++++++++++++ modules/shops/shop.routes.js | 16 +++++++----- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/modules/shops/shop.controller.js b/modules/shops/shop.controller.js index 26ca798..e0868ff 100644 --- a/modules/shops/shop.controller.js +++ b/modules/shops/shop.controller.js @@ -111,10 +111,55 @@ const getShopById = async (req, res, next) => { } }; +const createShop = async (req, res, next) => { + try { + const { + name, + description, + category, + lat, + lng, + address + } = req.body; + + // Validation + if (!name || !lat || !lng) { + return res.status(400).json({ + status: 'error', + message: 'Name, latitude, and longitude are required' + }); + } + + const shop = await Shop.create({ + owner: req.user.id, // from JWT middleware + name, + description, + category, + address, + location: { + type: 'Point', + coordinates: [ + parseFloat(lng), + parseFloat(lat) + ] + } + }); + + res.status(201).json({ + status: 'success', + message: 'Shop created successfully', + data: shop + }); + + } catch (error) { + next(error); + } +}; module.exports = { getShops, getNearbyShops, getShopById, + createShop, }; diff --git a/modules/shops/shop.routes.js b/modules/shops/shop.routes.js index baa9a5d..d2651dc 100644 --- a/modules/shops/shop.routes.js +++ b/modules/shops/shop.routes.js @@ -2,16 +2,16 @@ const express = require('express'); const router = express.Router(); const { isShopOwner,verifyToken,requireRole } = require('../../middleware/auth'); -const { getShops, getNearbyShops,getShopById } = require('./shop.controller'); +const { getShops, getNearbyShops,getShopById,createShop } = require('./shop.controller'); // TODO: Implement shops routes // Middleware: verifyToken, requireRole('seller'), isShopOwner // ── Shop routes ──────────────────────────────────────────────────────────── -// GET /api/shops — public: list shops (filter by category, isActive: true) / -// GET /api/shops/nearby — public: geo-query ?lat=&lng=&radius= (uses 2dsphere $near) -// GET /api/shops/:id — public: shop detail + product list -// POST /api/shops — seller: create shop with coords → pin appears on map + + + + // PUT /api/shops/:id — seller (isShopOwner): update shop info // POST /api/shops/:id/upload — seller: get pre-signed S3 URL for cover/logo upload @@ -20,11 +20,15 @@ const { getShops, getNearbyShops,getShopById } = require('./shop.controller'); // POST /api/shops/:id/reviews — buyer (verifyToken): submit a review // Public: List active shops (or add this query to filter category, isActive) +// GET /api/shops — public: list shops (filter by category, isActive: true) / router.get('/', getShops); +// GET /api/shops/nearby — public: geo-query ?lat=&lng=&radius= (uses 2dsphere $near) router.get('/nearby', getNearbyShops); +// GET /api/shops/:id — public: shop detail + product list router.get('/:id', getShopById); - +// POST /api/shops — seller: create shop with coords → pin appears on map +router.post('/', verifyToken, requireRole('seller'), createShop); module.exports = router; From adbb61c0ac7dfcfc7c9bec610b7d4d22af23686e Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 12:18:29 +0800 Subject: [PATCH 17/37] chore: add an upload cover and logo to the create shop --- modules/products/product.controller.js | 2 +- modules/shops/shop.controller.js | 21 +++++++++++++++++++-- modules/shops/shop.model.js | 2 +- modules/shops/shop.routes.js | 10 ++++++++-- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/modules/products/product.controller.js b/modules/products/product.controller.js index 3ea432f..023f044 100644 --- a/modules/products/product.controller.js +++ b/modules/products/product.controller.js @@ -33,7 +33,7 @@ const createProduct = async (req, res, next) => { let imageUrl = ''; - // catch by multer muna + // catch by multer if (req.file) { const uploadedImage = await uploadImage(req.file.path, 'products'); imageUrl = uploadedImage.secure_url; diff --git a/modules/shops/shop.controller.js b/modules/shops/shop.controller.js index e0868ff..c7c44f8 100644 --- a/modules/shops/shop.controller.js +++ b/modules/shops/shop.controller.js @@ -1,4 +1,5 @@ const Shop = require('./shop.model') +const { uploadImage } = require('../../services/uploader'); // TODO: Implement shops controller // Planned handlers: @@ -122,7 +123,6 @@ const createShop = async (req, res, next) => { address } = req.body; - // Validation if (!name || !lat || !lng) { return res.status(400).json({ status: 'error', @@ -130,12 +130,29 @@ const createShop = async (req, res, next) => { }); } + let coverUrl = ''; + let logoUrl = ''; + + + if (req.files?.cover) { + const cover = await uploadImage(req.files.cover[0].path, 'shops'); + coverUrl = cover.secure_url; + } + + + if (req.files?.logo) { + const logo = await uploadImage(req.files.logo[0].path, 'shops'); + logoUrl = logo.secure_url; + } + const shop = await Shop.create({ - owner: req.user.id, // from JWT middleware + owner: req.user.id, name, description, category, address, + coverUrl, + logoUrl, location: { type: 'Point', coordinates: [ diff --git a/modules/shops/shop.model.js b/modules/shops/shop.model.js index e9ebe9b..31f7e5f 100644 --- a/modules/shops/shop.model.js +++ b/modules/shops/shop.model.js @@ -10,7 +10,7 @@ const shopSchema = new mongoose.Schema( description: { type: String }, category: { type: String }, // used for map filter tabs - // ── Media (S3 keys: shops/{shopId}/cover, shops/{shopId}/logo) ──── + coverUrl: { type: String }, logoUrl: { type: String }, diff --git a/modules/shops/shop.routes.js b/modules/shops/shop.routes.js index d2651dc..24d01f5 100644 --- a/modules/shops/shop.routes.js +++ b/modules/shops/shop.routes.js @@ -3,6 +3,7 @@ const express = require('express'); const router = express.Router(); const { isShopOwner,verifyToken,requireRole } = require('../../middleware/auth'); const { getShops, getNearbyShops,getShopById,createShop } = require('./shop.controller'); +const upload = require('../../middleware/multer'); // TODO: Implement shops routes // Middleware: verifyToken, requireRole('seller'), isShopOwner @@ -24,11 +25,16 @@ const { getShops, getNearbyShops,getShopById,createShop } = require('./shop.cont router.get('/', getShops); // GET /api/shops/nearby — public: geo-query ?lat=&lng=&radius= (uses 2dsphere $near) +// example http://localhost:5000/api/shops/nearby?lat=8.228&lng=124.246&radius=5000 router.get('/nearby', getNearbyShops); -// GET /api/shops/:id — public: shop detail + product list +// GET /api/shops/:id — public: shop detail + product list +// example http://localhost:5000/api/shops/6a3213b4152aaded1ac9a0ff router.get('/:id', getShopById); // POST /api/shops — seller: create shop with coords → pin appears on map -router.post('/', verifyToken, requireRole('seller'), createShop); +router.post('/',verifyToken,requireRole('seller'),upload.fields([{ name: 'cover', maxCount: 1 },{ name: 'logo', maxCount: 1 }]),createShop); + + + module.exports = router; From cbc6673662b666dc0ef735c8e97b2715f1da5479 Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 13:17:16 +0800 Subject: [PATCH 18/37] feat: getreview --- modules/shops/review.model.js | 14 +++++ modules/shops/shop.controller.js | 100 +++++++++++++++++++++++++++++++ modules/shops/shop.routes.js | 15 ++++- 3 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 modules/shops/review.model.js diff --git a/modules/shops/review.model.js b/modules/shops/review.model.js new file mode 100644 index 0000000..7872854 --- /dev/null +++ b/modules/shops/review.model.js @@ -0,0 +1,14 @@ +const mongoose = require('mongoose'); + +const reviewSchema = new mongoose.Schema( + { + shop: { type: mongoose.Schema.Types.ObjectId, ref: 'Shop', required: true }, + user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + + rating: { type: Number, min: 1, max: 5, required: true }, + comment: { type: String } + }, + { timestamps: true } +); + +module.exports = mongoose.model('Review', reviewSchema); \ No newline at end of file diff --git a/modules/shops/shop.controller.js b/modules/shops/shop.controller.js index c7c44f8..2ac284f 100644 --- a/modules/shops/shop.controller.js +++ b/modules/shops/shop.controller.js @@ -1,4 +1,5 @@ const Shop = require('./shop.model') +const Review = require('./review.model'); const { uploadImage } = require('../../services/uploader'); // TODO: Implement shops controller @@ -173,10 +174,109 @@ const createShop = async (req, res, next) => { } }; +const updateShop = async (req, res, next) => { + try { + const { id } = req.params; + + const { + name, + description, + category, + lat, + lng, + address + } = req.body; + + const shop = await Shop.findById(id); + + if (!shop) { + return res.status(404).json({ + status: 'error', + message: 'Shop not found' + }); + } + + // ownership check (extra safety) + if (shop.owner.toString() !== req.user.id) { + return res.status(403).json({ + status: 'error', + message: 'Not allowed to update this shop' + }); + } + + // update text fields + if (name) shop.name = name; + if (description) shop.description = description; + if (category) shop.category = category; + if (address) shop.address = address; + + // update location + if (lat && lng) { + shop.location = { + type: 'Point', + coordinates: [ + parseFloat(lng), + parseFloat(lat) + ] + }; + } + + // 🖼️ UPDATE COVER IMAGE + if (req.files?.cover) { + const coverUpload = await uploadImage( + req.files.cover[0].path, + 'shops' + ); + shop.coverUrl = coverUpload.secure_url; + } + + // 🖼️ UPDATE LOGO IMAGE + if (req.files?.logo) { + const logoUpload = await uploadImage( + req.files.logo[0].path, + 'shops' + ); + shop.logoUrl = logoUpload.secure_url; + } + + const updatedShop = await shop.save(); + + res.status(200).json({ + status: 'success', + message: 'Shop updated successfully', + data: updatedShop + }); + + } catch (error) { + next(error); + } +}; + +const getReviews = async (req, res, next) => { + try { + const { id } = req.params; + + const reviews = await Review.find({ shop: id }) + .populate('user', 'name avatarUrl') + .sort({ createdAt: -1 }); + + res.status(200).json({ + status: 'success', + count: reviews.length, + data: reviews + }); + + } catch (error) { + next(error); + } +}; + module.exports = { getShops, getNearbyShops, getShopById, createShop, + updateShop, + getReviews, }; diff --git a/modules/shops/shop.routes.js b/modules/shops/shop.routes.js index 24d01f5..8563a0a 100644 --- a/modules/shops/shop.routes.js +++ b/modules/shops/shop.routes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { isShopOwner,verifyToken,requireRole } = require('../../middleware/auth'); -const { getShops, getNearbyShops,getShopById,createShop } = require('./shop.controller'); +const { getShops, getNearbyShops,getShopById,createShop,updateShop,getReviews } = require('./shop.controller'); const upload = require('../../middleware/multer'); // TODO: Implement shops routes @@ -13,13 +13,16 @@ const upload = require('../../middleware/multer'); -// PUT /api/shops/:id — seller (isShopOwner): update shop info + // POST /api/shops/:id/upload — seller: get pre-signed S3 URL for cover/logo upload // ── Review sub-routes (nested under shops) ───────────────────────────────── -// GET /api/shops/:id/reviews — public: list reviews for a shop (seeded for demo) + // POST /api/shops/:id/reviews — buyer (verifyToken): submit a review + + + // Public: List active shops (or add this query to filter category, isActive) // GET /api/shops — public: list shops (filter by category, isActive: true) / router.get('/', getShops); @@ -35,6 +38,12 @@ router.get('/:id', getShopById); // POST /api/shops — seller: create shop with coords → pin appears on map router.post('/',verifyToken,requireRole('seller'),upload.fields([{ name: 'cover', maxCount: 1 },{ name: 'logo', maxCount: 1 }]),createShop); +// PUT /api/shops/:id — seller (isShopOwner): update shop info +// EXAMPLE http://localhost:5000/api/shops/6a321e72c43e01b6215a55ad +router.put('/:id',verifyToken,requireRole('seller'),isShopOwner,upload.fields([{ name: 'cover', maxCount: 1 },{ name: 'logo', maxCount: 1 }]),updateShop); +// GET /api/shops/:id/reviews — public: list reviews for a shop (seeded for demo) +router.get('/:id/reviews', getReviews); + module.exports = router; From 15eea3fd92e6a9f18daeff4089d652a0f6eaeae5 Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 15:53:34 +0800 Subject: [PATCH 19/37] feat: added a Add_review to shop --- modules/auth/auth.routes.js | 8 ++-- modules/shops/shop.controller.js | 72 ++++++++++++++++++++++++++++++-- modules/shops/shop.routes.js | 12 +++++- 3 files changed, 83 insertions(+), 9 deletions(-) diff --git a/modules/auth/auth.routes.js b/modules/auth/auth.routes.js index 1b2b3de..82db909 100644 --- a/modules/auth/auth.routes.js +++ b/modules/auth/auth.routes.js @@ -3,7 +3,7 @@ const express = require('express'); const router = express.Router(); const upload = require('../../middleware/multer'); const { verifyToken } = require('../../middleware/auth') -// POST /api/auth/register — create account, assign role, return JWT (okay na to) +// POST /api/auth/register — create account, assign role, return JWT // POST /api/auth/login — verify credentials, return JWT + user object // GET /api/auth/me — protected: return current user from token (session restore) @@ -15,7 +15,7 @@ const { } = require('./auth.controller'); // POST /api/auth/register -router.post('/register',upload.single('image'), register); +router.post('/register',upload.single('avatarUrl'), register); // Example payload { // "name": "Juan Dela Cruz", // "email": "juan@example.com", @@ -27,7 +27,7 @@ router.post('/register',upload.single('image'), register); - +// POST /api/auth/login router.post('/login', login); // Example Payload{ // "email": "juan@example.com", @@ -35,7 +35,7 @@ router.post('/login', login); // } -// POST /api/auth/login +// GET /api/auth/me router.get('/me',verifyToken, getMe); diff --git a/modules/shops/shop.controller.js b/modules/shops/shop.controller.js index 2ac284f..b15d713 100644 --- a/modules/shops/shop.controller.js +++ b/modules/shops/shop.controller.js @@ -204,13 +204,12 @@ const updateShop = async (req, res, next) => { }); } - // update text fields if (name) shop.name = name; if (description) shop.description = description; if (category) shop.category = category; if (address) shop.address = address; - // update location + if (lat && lng) { shop.location = { type: 'Point', @@ -221,7 +220,7 @@ const updateShop = async (req, res, next) => { }; } - // 🖼️ UPDATE COVER IMAGE + if (req.files?.cover) { const coverUpload = await uploadImage( req.files.cover[0].path, @@ -271,6 +270,72 @@ const getReviews = async (req, res, next) => { } }; +const createReview = async (req, res, next) => { + try { + console.log('BODY:', req.body); + console.log('HEADERS:', req.headers); + const { id } = req.params; + const { rating, comment } = req.body; + + + if (!rating) { + return res.status(400).json({ + status: 'error', + message: 'Rating is required' + }); + } + + + const shop = await Shop.findById(id); + + if (!shop) { + return res.status(404).json({ + status: 'error', + message: 'Shop not found' + }); + } + + + const existingReview = await Review.findOne({ + shop: id, + user: req.user.id + }); + + if (existingReview) { + return res.status(400).json({ + status: 'error', + message: 'You already reviewed this shop' + }); + } + + // create review + const review = await Review.create({ + shop: id, + user: req.user.id, + rating, + comment + }); + + // OPTIONAL: update shop rating (simple average) + const reviews = await Review.find({ shop: id }); + + const avgRating = + reviews.reduce((acc, r) => acc + r.rating, 0) / reviews.length; + + shop.rating = avgRating; + await shop.save(); + + res.status(201).json({ + status: 'success', + message: 'Review added successfully', + data: review + }); + + } catch (error) { + next(error); + } +}; + module.exports = { getShops, @@ -279,4 +344,5 @@ module.exports = { createShop, updateShop, getReviews, + createReview, }; diff --git a/modules/shops/shop.routes.js b/modules/shops/shop.routes.js index 8563a0a..0e85758 100644 --- a/modules/shops/shop.routes.js +++ b/modules/shops/shop.routes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { isShopOwner,verifyToken,requireRole } = require('../../middleware/auth'); -const { getShops, getNearbyShops,getShopById,createShop,updateShop,getReviews } = require('./shop.controller'); +const { getShops, getNearbyShops,getShopById,createShop,updateShop,getReviews,createReview } = require('./shop.controller'); const upload = require('../../middleware/multer'); // TODO: Implement shops routes @@ -18,7 +18,7 @@ const upload = require('../../middleware/multer'); // ── Review sub-routes (nested under shops) ───────────────────────────────── -// POST /api/shops/:id/reviews — buyer (verifyToken): submit a review + @@ -46,4 +46,12 @@ router.put('/:id',verifyToken,requireRole('seller'),isShopOwner,upload.fields([{ // GET /api/shops/:id/reviews — public: list reviews for a shop (seeded for demo) router.get('/:id/reviews', getReviews); +// POST /api/shops/:id/reviews — buyer (verifyToken): submit a review +router.post('/:id/add_reviews',verifyToken,requireRole('buyer'),createReview); +// example payload{ +// { +// "rating" : 5, +// "comment" : "wow" +// } +// } module.exports = router; From 8c35ef0dfc31136affcec2dbe43d61feecb548cd Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 21:23:41 +0800 Subject: [PATCH 20/37] feat: added a edit product add: isproductowner in middlewere --- middleware/auth.js | 45 +++++++++- modules/products/product.controller.js | 118 ++++++++++++++++++++++++- modules/products/product.routes.js | 34 +++++-- 3 files changed, 186 insertions(+), 11 deletions(-) diff --git a/middleware/auth.js b/middleware/auth.js index f231b6b..5f12185 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -2,7 +2,8 @@ const jwt = require('jsonwebtoken'); const Shop = require('../modules/shops/shop.model'); // adjust path if needed -const Order = require('../modules/orders/order.model'); // adjust if exists +const Order = require('../modules/orders/order.model'); // adjust if existsProduct is not defined +const Product = require('../modules/products/product.model'); /** * verifyToken * Checks Authorization: Bearer , decodes it, @@ -48,7 +49,7 @@ const requireRole = (...roles) => { return (req, res, next) => { if (!req.user) { return res.status(401).json({ - status: 'error', + status: `error ${req.user}`, message: 'Unauthorized' }); } @@ -104,6 +105,45 @@ const isShopOwner = async (req, res, next) => { } }; +const isProductOwner = async (req, res, next) => { + try { + const { id } = req.params; + + const product = await Product.findById(id); + + if (!product) { + return res.status(404).json({ + status: 'error', + message: 'Product not found' + }); + } + + const shop = await Shop.findById(product.shop); + + if (!shop) { + return res.status(404).json({ + status: 'error', + message: 'Shop not found' + }); + } + + if (shop.owner.toString() !== req.user.id) { + return res.status(403).json({ + status: 'error', + message: 'You are not the owner of this product' + }); + } + + req.product = product; + req.shop = shop; + + next(); + + } catch (error) { + next(error); + } +}; + /** * isOrderOwner * Confirms order.buyer === req.user.id (buyer access) @@ -187,4 +227,5 @@ module.exports = { isShopOwner, isOrderOwner, verifyWebhookSignature, + isProductOwner, }; diff --git a/modules/products/product.controller.js b/modules/products/product.controller.js index 023f044..e5c89d1 100644 --- a/modules/products/product.controller.js +++ b/modules/products/product.controller.js @@ -8,6 +8,8 @@ // moderateProduct — admin: approve or reject product listing const Product = require('./product.model'); +const Shop = require('../shops/shop.model'); // adjust path if needed + const { uploadImage } = require('../../services/uploader'); const createProduct = async (req, res, next) => { @@ -62,8 +64,122 @@ const createProduct = async (req, res, next) => { next(error); } }; +const getShopProducts = async (req, res, next) => { + try { + const { shopId } = req.params; + + const products = await Product.find({ shop: shopId }); + + res.status(200).json({ + status: 'success', + results: products.length, + data: products + }); + + } catch (error) { + next(error); + } +}; + +const getProductBySlug = async (req, res, next) => { + try { + const { id } = req.params; + + const product = await Product.findById(id).populate('shop'); + + if (!product) { + return res.status(404).json({ + status: 'error', + message: 'Product not found' + }); + } + + res.status(200).json({ + status: 'success', + data: product + }); + + } catch (error) { + next(error); + } +}; + +const updateProduct = async (req, res, next) => { + try { + const { id } = req.params; + + const product = await Product.findById(id); + + if (!product) { + return res.status(404).json({ + status: 'error', + message: 'Product not found' + }); + } + + // 🔐 Check ownership (seller must own the shop) + const shop = await Shop.findById(product.shop); + + if (!shop || shop.owner.toString() !== req.user.id) { + return res.status(403).json({ + status: 'error', + message: 'You are not allowed to edit this product' + }); + } + + const { + name, + description, + price, + category, + colors, + sizes, + stockQuantity + } = req.body; + + const updates = {}; + + if (name) updates.name = name; + if (description) updates.description = description; + if (price !== undefined) updates.price = price; + if (category) updates.category = category; + if (colors) updates.colors = colors; + if (sizes) updates.sizes = sizes; + if (stockQuantity !== undefined) { + updates.stockQuantity = stockQuantity; + updates.inStock = stockQuantity > 0; + } + + // 📸 Image update (Cloudinary) + if (req.file) { + const uploadedImage = await uploadImage(req.file.path, 'products'); + updates.imageUrl = uploadedImage.secure_url; + } + + const updatedProduct = await Product.findByIdAndUpdate( + id, + updates, + { + new: true, + runValidators: true + } + ); + + res.status(200).json({ + status: 'success', + message: 'Product updated successfully', + data: updatedProduct + }); + + } catch (error) { + next(error); + } +}; module.exports = { - createProduct + createProduct, + getShopProducts, + getProductBySlug, + updateProduct }; diff --git a/modules/products/product.routes.js b/modules/products/product.routes.js index 7576ab7..af80183 100644 --- a/modules/products/product.routes.js +++ b/modules/products/product.routes.js @@ -1,38 +1,56 @@ // TODO: Implement products routes const express = require('express'); const router = express.Router(); +const { isShopOwner,verifyToken,requireRole,isProductOwner } = require('../../middleware/auth'); const upload = require('../../middleware/multer'); // Middleware: verifyToken, requireRole('seller'), isShopOwner -// GET /api/shops/:shopId/products — public: list products for a shop -// GET /api/products/:id — public: single product detail -// POST /api/shops/:shopId/products — seller: add product to shop -// PUT /api/products/:id — seller (isShopOwner): edit product details + + + + // DELETE /api/products/:id — seller (isShopOwner): remove product // POST /api/products/:id/upload — seller: get pre-signed S3 URL for product image const { - createProduct + createProduct, + getShopProducts, + getProductBySlug, + updateProduct } = require('./product.controller'); +const { verify } = require('jsonwebtoken'); + /* POST /api/shops/:shopId/products */ - -router.post('/shops/:shopId/products', upload.single('image'), createProduct); +// POST /api/shops/:shopId/products — seller: add product to shop +router.post('/shops/:shopId/products',verifyToken ,requireRole('seller'), upload.single('image'), createProduct); // Example payload { // "name": "Wooden Chair", // "description": "Handmade premium wooden chair made from solid mahogany wood", // "price": 2500, // "category": "Furniture", -// "imageUrl": "https://res.cloudinary.com/demo/image/upload/sample.jpg", (hindi gagana if raw json need mo ng file handler) +// "imageUrl": "https://res.cloudinary.com/demo/image/upload/sample.jpg", (hindi gagana if raw json) // "colors": ["Brown", "Dark Brown"], // "sizes": ["Small", "Medium"], // "stockQuantity": 10 // } +// GET /api/shops/:shopId/get_products — public: list products for a shop +// example request http://localhost:5000/api/shops/66a2d5e1568fe284d486a562/get_products +router.get('/shops/:shopId/get_products', getShopProducts); + + + +// GET /api/products/:id — public: single product detail +router.get('/products/:id', getProductBySlug); + + +// PUT /api/products/:id — seller (isShopOwner): edit product details +router.put('/products/:id/edit_product', verifyToken , requireRole('seller') , isProductOwner , upload.single('image') , updateProduct); module.exports = router; From 8de73a98bc3c1c46a6500175f1f79d7cc3ae0f14 Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 21:49:56 +0800 Subject: [PATCH 21/37] feat: Remove product --- modules/products/product.controller.js | 30 ++++++++++++++++++++++++-- modules/products/product.routes.js | 9 ++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/modules/products/product.controller.js b/modules/products/product.controller.js index e5c89d1..ba8f311 100644 --- a/modules/products/product.controller.js +++ b/modules/products/product.controller.js @@ -117,7 +117,7 @@ const updateProduct = async (req, res, next) => { }); } - // 🔐 Check ownership (seller must own the shop) + const shop = await Shop.findById(product.shop); if (!shop || shop.owner.toString() !== req.user.id) { @@ -176,10 +176,36 @@ const updateProduct = async (req, res, next) => { } }; +const deleteProduct = async (req, res, next) => { + try { + const { id } = req.params; + + const product = await Product.findById(id); + + if (!product) { + return res.status(404).json({ + status: 'error', + message: 'Product not found' + }); + } + + await Product.findByIdAndDelete(id); + + res.status(200).json({ + status: 'success', + message: 'Product deleted successfully' + }); + + } catch (error) { + next(error); + } +}; + module.exports = { createProduct, getShopProducts, getProductBySlug, - updateProduct + updateProduct, + deleteProduct }; diff --git a/modules/products/product.routes.js b/modules/products/product.routes.js index af80183..d899492 100644 --- a/modules/products/product.routes.js +++ b/modules/products/product.routes.js @@ -9,7 +9,7 @@ const upload = require('../../middleware/multer'); -// DELETE /api/products/:id — seller (isShopOwner): remove product + // POST /api/products/:id/upload — seller: get pre-signed S3 URL for product image @@ -17,7 +17,8 @@ const { createProduct, getShopProducts, getProductBySlug, - updateProduct + updateProduct, + deleteProduct } = require('./product.controller'); const { verify } = require('jsonwebtoken'); @@ -53,4 +54,8 @@ router.get('/products/:id', getProductBySlug); // PUT /api/products/:id — seller (isShopOwner): edit product details router.put('/products/:id/edit_product', verifyToken , requireRole('seller') , isProductOwner , upload.single('image') , updateProduct); + +// DELETE /api/products/:id — seller (isShopOwner): remove product +router.delete('/products/:id/delete_product',verifyToken,requireRole('seller'),isProductOwner,deleteProduct); + module.exports = router; From 183919d6d5508adbba516ca65b3ea4831bd12820 Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 23:03:15 +0800 Subject: [PATCH 22/37] feat: established the payment gateway paymong --- modules/payments/payment.controller.js | 125 ++++++++++++++++++++++++- modules/payments/payment.routes.js | 17 ++++ services/paymongo.js | 13 +++ 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 services/paymongo.js diff --git a/modules/payments/payment.controller.js b/modules/payments/payment.controller.js index c82cc05..4a49868 100644 --- a/modules/payments/payment.controller.js +++ b/modules/payments/payment.controller.js @@ -1,3 +1,9 @@ +const Payment = require('./payment.model'); +const Order = require('../orders/order.model'); +const paymongo = require('../../services/paymongo'); +const { checkout } = require('./payment.routes'); + + // TODO: Implement payments controller // Planned handlers: // createCheckout — create a Stripe or PayMongo checkout session for an order @@ -6,4 +12,121 @@ // getPaymentStatus — return payment status for a given order // refundPayment — admin: trigger a refund via gateway API -module.exports = {}; +const createCheckout = async (req, res, next) => { + try { + const { orderId } = req.body; + + const order = await Order.findById(orderId).populate('items.product'); + + if (!order) { + return res.status(404).json({ + status: 'error', + message: 'Order not found' + }); + } + + // Convert to centavos + const amount = order.totalPrice * 100; + + const response = await paymongo.post('/checkout_sessions', { + data: { + attributes: { + billing: { + name: req.user.name, + email: req.user.email + }, + line_items: order.items.map(item => ({ + name: item.product.name, + quantity: item.quantity, + amount: item.price * 100, + currency: 'PHP' + })), + payment_method_types: ['gcash', 'card'], + amount: amount, + description: `Order ${order._id}`, + success_url: 'http://localhost:3000/success', + cancel_url: 'http://localhost:3000/cancel' + } + } + }); + + const checkoutUrl = response.data.data.attributes.checkout_url; + const linkId = response.data.data.id; + + const payment = await Payment.create({ + order: orderId, + buyer: req.user.id, + paymongoLinkId: linkId, + checkoutUrl, + amount, + status: 'pending' + }); + + res.status(201).json({ + status: 'success', + checkoutUrl, + payment + }); + + } catch (error) { + next(error); + } +}; + +const stripeWebhook = async (req, res) => { + // PayMongo uses webhooks, not Stripe naming + const event = req.body; + + try { + if (event.data.attributes.type === 'checkout_session.payment.paid') { + + const linkId = event.data.attributes.data.id; + + const payment = await Payment.findOne({ + paymongoLinkId: linkId + }); + + if (!payment) return res.sendStatus(404); + + payment.status = 'paid'; + payment.paidAt = new Date(); + payment.webhookPayload = event; + + await payment.save(); + + await Order.findByIdAndUpdate(payment.order, { + status: 'confirmed' + }); + } + + res.sendStatus(200); + + } catch (err) { + res.sendStatus(500); + } +}; + +const getPaymentStatus = async (req, res, next) => { + try { + const { orderId } = req.params; + + const payment = await Payment.findOne({ order: orderId }); + + if (!payment) { + return res.status(404).json({ + status: 'error', + message: 'Payment not found' + }); + } + + res.json({ + status: 'success', + data: payment + }); + + } catch (error) { + next(error); + } +}; + +module.exports = {checkout,stripeWebhook,getPaymentStatus,createCheckout }; diff --git a/modules/payments/payment.routes.js b/modules/payments/payment.routes.js index 5d1b19f..4cb0bbe 100644 --- a/modules/payments/payment.routes.js +++ b/modules/payments/payment.routes.js @@ -8,4 +8,21 @@ const router = express.Router(); // requires verifyWebhookSignature middleware // GET /api/payments/:orderId/status — buyer: check current payment status for an order + + +const { + createCheckout, + getPaymentStatus, + stripeWebhook +} = require('./payment.controller'); + +const { verifyToken } = require('../../middleware/auth'); + +router.post('/create-link', verifyToken, createCheckout); + +router.get('/:orderId/status', verifyToken, getPaymentStatus); + +router.post('/webhook', stripeWebhook); + module.exports = router; + diff --git a/services/paymongo.js b/services/paymongo.js new file mode 100644 index 0000000..1ad3175 --- /dev/null +++ b/services/paymongo.js @@ -0,0 +1,13 @@ +const axios = require('axios'); + +const PAYMONGO_SECRET = process.env.PAYMONGO_SECRET_KEY; + +const paymongo = axios.create({ + baseURL: 'https://api.paymongo.com/v1', + headers: { + Authorization: `Basic ${Buffer.from(PAYMONGO_SECRET + ':').toString('base64')}`, + 'Content-Type': 'application/json' + } +}); + +module.exports = paymongo; \ No newline at end of file From a9542b92db204ee8e17f47f32f3592a93b76ec85 Mon Sep 17 00:00:00 2001 From: Yassef Date: Wed, 17 Jun 2026 23:52:27 +0800 Subject: [PATCH 23/37] feat: place order --- modules/orders/order.controller.js | 71 +++++++++++++++++++++- modules/orders/order.routes.js | 30 +++++++++- modules/payments/payment.controller.js | 81 ++++++++++++++++++++++---- modules/payments/payment.routes.js | 9 ++- server.js | 1 + 5 files changed, 176 insertions(+), 16 deletions(-) diff --git a/modules/orders/order.controller.js b/modules/orders/order.controller.js index 6e67cc1..619b182 100644 --- a/modules/orders/order.controller.js +++ b/modules/orders/order.controller.js @@ -8,5 +8,74 @@ // deliverOrder — seller: shipped → delivered (marks as complete) // cancelOrder — buyer or seller: any pre-shipped status → cancelled +const Order = require('./order.model'); +const Product = require('../products/product.model'); -module.exports = {}; +const placeOrder = async (req, res, next) => { + try { + const { + shopId, + items, + deliveryAddress, + deliveryNotes, + shippingFee = 0, + paymentMethod = 'cod' + } = req.body; + + if (!shopId || !items || items.length === 0) { + return res.status(400).json({ + status: 'error', + message: 'Shop and items are required' + }); + } + + let total = 0; + const orderItems = []; + + for (const item of items) { + const product = await Product.findById(item.productId); + + if (!product) { + return res.status(404).json({ + status: 'error', + message: `Product not found: ${item.productId}` + }); + } + + const itemTotal = product.price * item.quantity; + total += itemTotal; + + orderItems.push({ + product: product._id, + name: product.name, + price: product.price, + quantity: item.quantity, + color: item.color, + size: item.size + }); + } + + total += shippingFee; + + const order = await Order.create({ + buyer: req.user.id, + shop: shopId, + items: orderItems, + total, + deliveryAddress, + deliveryNotes, + shippingFee, + paymentMethod + }); + + res.status(201).json({ + status: 'success', + message: 'Order created successfully', + data: order + }); + + } catch (error) { + next(error); + } +}; +module.exports = { placeOrder }; diff --git a/modules/orders/order.routes.js b/modules/orders/order.routes.js index 096c69d..cf39999 100644 --- a/modules/orders/order.routes.js +++ b/modules/orders/order.routes.js @@ -1,7 +1,8 @@ // TODO: Implement orders routes const express = require('express'); const router = express.Router(); - +const { verifyToken, requireRole } = require('../../middleware/auth'); +const { placeOrder } = require('./order.controller'); // Middleware: verifyToken, requireRole, isOrderOwner // POST /api/orders — buyer: place order (snapshot items, compute total) @@ -12,4 +13,29 @@ const router = express.Router(); // PUT /api/orders/:id/deliver — seller: shipped → delivered // PUT /api/orders/:id/cancel — buyer or seller: cancel (only pre-shipped) -module.exports = router; + + + + +router.post('/', verifyToken , requireRole('buyer'), placeOrder); +// payload example +// { +// "shopId": "6a321e72c43e01b6215a55ad", +// "items": [ +// { +// "productId": "6a2f006e4a3ebb1aa8f276c7", +// "quantity": 2, +// "color": "Brown", +// "size": "M" +// } +// ], +// "deliveryAddress": "Quiapo, Manila", +// "deliveryNotes": "Call me before delivery", +// "shippingFee": 50, +// "paymentMethod": "gcash" +// } + + + + +module.exports = router; \ No newline at end of file diff --git a/modules/payments/payment.controller.js b/modules/payments/payment.controller.js index 4a49868..b2b93ce 100644 --- a/modules/payments/payment.controller.js +++ b/modules/payments/payment.controller.js @@ -16,6 +16,14 @@ const createCheckout = async (req, res, next) => { try { const { orderId } = req.body; + if (!orderId) { + return res.status(400).json({ + status: 'error', + message: 'orderId is required' + }); + } + + // 1. Find order const order = await Order.findById(orderId).populate('items.product'); if (!order) { @@ -25,9 +33,18 @@ const createCheckout = async (req, res, next) => { }); } - // Convert to centavos - const amount = order.totalPrice * 100; + // 2. Ensure buyer owns the order + if (order.buyer.toString() !== req.user.id) { + return res.status(403).json({ + status: 'error', + message: 'Not allowed to pay for this order' + }); + } + + // 3. Convert to centavos (your schema already uses centavos) + const amount = order.total; + // 4. Create PayMongo checkout session const response = await paymongo.post('/checkout_sessions', { data: { attributes: { @@ -36,14 +53,13 @@ const createCheckout = async (req, res, next) => { email: req.user.email }, line_items: order.items.map(item => ({ - name: item.product.name, + name: item.name, quantity: item.quantity, - amount: item.price * 100, + amount: item.price, // already centavos currency: 'PHP' })), payment_method_types: ['gcash', 'card'], - amount: amount, - description: `Order ${order._id}`, + description: `Payment for Order ${order._id}`, success_url: 'http://localhost:3000/success', cancel_url: 'http://localhost:3000/cancel' } @@ -53,8 +69,9 @@ const createCheckout = async (req, res, next) => { const checkoutUrl = response.data.data.attributes.checkout_url; const linkId = response.data.data.id; + // 5. Save payment record const payment = await Payment.create({ - order: orderId, + order: order._id, buyer: req.user.id, paymongoLinkId: linkId, checkoutUrl, @@ -64,8 +81,11 @@ const createCheckout = async (req, res, next) => { res.status(201).json({ status: 'success', - checkoutUrl, - payment + message: 'Payment link created successfully', + data: { + checkoutUrl, + payment + } }); } catch (error) { @@ -119,7 +139,7 @@ const getPaymentStatus = async (req, res, next) => { }); } - res.json({ + res.status(200).json({ status: 'success', data: payment }); @@ -128,5 +148,44 @@ const getPaymentStatus = async (req, res, next) => { next(error); } }; +const paymongoWebhook = async (req, res) => { + try { + const event = req.body; + + const eventType = event.data.attributes.type; + + if (eventType === 'checkout_session.payment.paid') { + + const linkId = event.data.attributes.data.id; -module.exports = {checkout,stripeWebhook,getPaymentStatus,createCheckout }; + const payment = await Payment.findOne({ + paymongoLinkId: linkId + }); + + if (!payment) return res.sendStatus(404); + + payment.status = 'paid'; + payment.paidAt = new Date(); + payment.webhookPayload = event; + + await payment.save(); + + // update order + await Order.findByIdAndUpdate(payment.order, { + status: 'confirmed' + }); + } + + res.sendStatus(200); + + } catch (error) { + console.error(error); + res.sendStatus(500); + } +}; + +module.exports = { + createCheckout, + getPaymentStatus, + paymongoWebhook +}; diff --git a/modules/payments/payment.routes.js b/modules/payments/payment.routes.js index 4cb0bbe..9f95713 100644 --- a/modules/payments/payment.routes.js +++ b/modules/payments/payment.routes.js @@ -10,19 +10,24 @@ const router = express.Router(); + + const { createCheckout, getPaymentStatus, - stripeWebhook + paymongoWebhook } = require('./payment.controller'); const { verifyToken } = require('../../middleware/auth'); +// create payment link router.post('/create-link', verifyToken, createCheckout); +// check payment status router.get('/:orderId/status', verifyToken, getPaymentStatus); -router.post('/webhook', stripeWebhook); +// webhook (NO auth middleware) +router.post('/webhook', paymongoWebhook); module.exports = router; diff --git a/server.js b/server.js index 4a94399..5262888 100644 --- a/server.js +++ b/server.js @@ -28,6 +28,7 @@ app.get('/api/health', (req, res) => { // app.use('/api/auth', require('./modules/auth/routes')); // app.use('/api/payments', require('./modules/payments/routes')); +app.use('/api/order', require('./modules/orders/order.routes')); app.use('/api/shops', require('./modules/shops/shop.routes')); app.use('/api/users', require('./modules/users/user.routes')); From 73bc88c920f039c3102aaca4abc00745be1cd91b Mon Sep 17 00:00:00 2001 From: Yassef Date: Thu, 18 Jun 2026 18:14:34 +0800 Subject: [PATCH 24/37] feat: added a getMyOrders --- modules/orders/order.controller.js | 21 ++++++++++++++++++++- modules/orders/order.routes.js | 4 +++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/modules/orders/order.controller.js b/modules/orders/order.controller.js index 619b182..1b8f7c3 100644 --- a/modules/orders/order.controller.js +++ b/modules/orders/order.controller.js @@ -78,4 +78,23 @@ const placeOrder = async (req, res, next) => { next(error); } }; -module.exports = { placeOrder }; +const getMyOrders = async (req, res, next) => { + try { + + const orders = await Order.find({ + buyer: req.user.id + }) + .populate('shop', 'name') + .sort({ createdAt: -1 }); + + res.status(200).json({ + status: 'success', + count: orders.length, + data: orders + }); + + } catch (error) { + next(error); + } +}; +module.exports = { placeOrder,getMyOrders }; diff --git a/modules/orders/order.routes.js b/modules/orders/order.routes.js index cf39999..07f3160 100644 --- a/modules/orders/order.routes.js +++ b/modules/orders/order.routes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole } = require('../../middleware/auth'); -const { placeOrder } = require('./order.controller'); +const { placeOrder,getMyOrders } = require('./order.controller'); // Middleware: verifyToken, requireRole, isOrderOwner // POST /api/orders — buyer: place order (snapshot items, compute total) @@ -36,6 +36,8 @@ router.post('/', verifyToken , requireRole('buyer'), placeOrder); // } +router.get('/my',verifyToken,requireRole('buyer'),getMyOrders); +// http://localhost:5000/api/order/my nuyer only module.exports = router; \ No newline at end of file From 08cbc9042f5b71153ec9d471e36951c085fad979 Mon Sep 17 00:00:00 2001 From: Yassef Date: Thu, 18 Jun 2026 18:24:01 +0800 Subject: [PATCH 25/37] feat : incoming orders for their shop --- modules/orders/order.controller.js | 23 ++++++++++++++++++++++- modules/orders/order.routes.js | 11 ++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/modules/orders/order.controller.js b/modules/orders/order.controller.js index 1b8f7c3..ec7b360 100644 --- a/modules/orders/order.controller.js +++ b/modules/orders/order.controller.js @@ -97,4 +97,25 @@ const getMyOrders = async (req, res, next) => { next(error); } }; -module.exports = { placeOrder,getMyOrders }; +const getShopOrders = async (req, res, next) => { + try { + const { id } = req.params; + + const orders = await Order.find({ shop: id }) + .populate('buyer', 'name email') + .populate('items.product', 'name price imageUrl') + .sort({ createdAt: -1 }); + + res.status(200).json({ + status: 'success', + count: orders.length, + data: orders + }); + + } catch (error) { + next(error); + } + + +}; +module.exports = { placeOrder,getMyOrders, getShopOrders }; diff --git a/modules/orders/order.routes.js b/modules/orders/order.routes.js index 07f3160..da8000e 100644 --- a/modules/orders/order.routes.js +++ b/modules/orders/order.routes.js @@ -1,8 +1,8 @@ // TODO: Implement orders routes const express = require('express'); const router = express.Router(); -const { verifyToken, requireRole } = require('../../middleware/auth'); -const { placeOrder,getMyOrders } = require('./order.controller'); +const { verifyToken, requireRole,isShopOwner } = require('../../middleware/auth'); +const { placeOrder,getMyOrders,getShopOrders } = require('./order.controller'); // Middleware: verifyToken, requireRole, isOrderOwner // POST /api/orders — buyer: place order (snapshot items, compute total) @@ -35,9 +35,14 @@ router.post('/', verifyToken , requireRole('buyer'), placeOrder); // "paymentMethod": "gcash" // } - +// GET /api/orders/my — buyer: own order history router.get('/my',verifyToken,requireRole('buyer'),getMyOrders); // http://localhost:5000/api/order/my nuyer only + + +// GET /api/shops/:id/orders — seller: incoming orders for their shop +router.get('/shops/:id/orders',verifyToken,requireRole('seller'),isShopOwner,getShopOrders); +// example http://localhost:5000/api/order/shops/6a3213b4152aaded1ac9a0ff/orders this should be seller module.exports = router; \ No newline at end of file From d8e1c76889e9ad5d4580507f66a0343b721af649 Mon Sep 17 00:00:00 2001 From: Yassef Date: Thu, 18 Jun 2026 18:32:39 +0800 Subject: [PATCH 26/37] =?UTF-8?q?feat:=20seller:=20pending=20=E2=86=92=20c?= =?UTF-8?q?onfirmed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/orders/order.controller.js | 47 ++++++++++++++++++++++++++++-- modules/orders/order.routes.js | 12 +++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/modules/orders/order.controller.js b/modules/orders/order.controller.js index ec7b360..1995676 100644 --- a/modules/orders/order.controller.js +++ b/modules/orders/order.controller.js @@ -116,6 +116,49 @@ const getShopOrders = async (req, res, next) => { next(error); } - + +}; + +const confirmOrder = async (req, res, next) => { + try { + const { id } = req.params; + + const order = await Order.findById(id).populate('shop'); + + if (!order) { + return res.status(404).json({ + status: 'error', + message: 'Order not found' + }); + } + + // ensure seller owns the shop + if (order.shop.owner.toString() !== req.user.id) { + return res.status(403).json({ + status: 'error', + message: 'Not authorized to confirm this order' + }); + } + + // status validation + if (order.status !== 'pending') { + return res.status(400).json({ + status: 'error', + message: `Cannot confirm order with status: ${order.status}` + }); + } + + order.status = 'confirmed'; + await order.save(); + + res.status(200).json({ + status: 'success', + message: 'Order confirmed successfully', + data: order + }); + + } catch (error) { + next(error); + } }; -module.exports = { placeOrder,getMyOrders, getShopOrders }; +module.exports = { placeOrder,getMyOrders, getShopOrders,confirmOrder }; diff --git a/modules/orders/order.routes.js b/modules/orders/order.routes.js index da8000e..18ecc96 100644 --- a/modules/orders/order.routes.js +++ b/modules/orders/order.routes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole,isShopOwner } = require('../../middleware/auth'); -const { placeOrder,getMyOrders,getShopOrders } = require('./order.controller'); +const { placeOrder,getMyOrders,getShopOrders,confirmOrder } = require('./order.controller'); // Middleware: verifyToken, requireRole, isOrderOwner // POST /api/orders — buyer: place order (snapshot items, compute total) @@ -45,4 +45,14 @@ router.get('/my',verifyToken,requireRole('buyer'),getMyOrders); // GET /api/shops/:id/orders — seller: incoming orders for their shop router.get('/shops/:id/orders',verifyToken,requireRole('seller'),isShopOwner,getShopOrders); // example http://localhost:5000/api/order/shops/6a3213b4152aaded1ac9a0ff/orders this should be seller + + + +// PUT /api/orders/:id/confirm — seller: pending → confirmed +router.put('/:id/confirm',verifyToken,requireRole('seller'),confirmOrder); +// GET http://localhost:5000/api/order/shops/6a3213b4152aaded1ac9a0ff/orders + + + + module.exports = router; \ No newline at end of file From 6eb220bad19de4bcaef6692cefccf9ec6930f8ab Mon Sep 17 00:00:00 2001 From: Yassef Date: Thu, 18 Jun 2026 18:41:41 +0800 Subject: [PATCH 27/37] feat: confirm to shipped --- modules/orders/order.controller.js | 55 +++++++++++++++++++++++++++++- modules/orders/order.routes.js | 9 +++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/modules/orders/order.controller.js b/modules/orders/order.controller.js index 1995676..f875de7 100644 --- a/modules/orders/order.controller.js +++ b/modules/orders/order.controller.js @@ -161,4 +161,57 @@ const confirmOrder = async (req, res, next) => { next(error); } }; -module.exports = { placeOrder,getMyOrders, getShopOrders,confirmOrder }; +const shipOrder = async (req, res, next) => { + try { + const { id } = req.params; + const { trackingNumber, courier } = req.body; + + if (!trackingNumber || !courier) { + return res.status(400).json({ + status: 'error', + message: 'Tracking number and courier are required' + }); + } + + const order = await Order.findById(id).populate('shop'); + + if (!order) { + return res.status(404).json({ + status: 'error', + message: 'Order not found' + }); + } + + // check ownership + if (order.shop.owner.toString() !== req.user.id) { + return res.status(403).json({ + status: 'error', + message: 'Not authorized to ship this order' + }); + } + + // status validation + if (order.status !== 'confirmed') { + return res.status(400).json({ + status: 'error', + message: `Order must be confirmed before shipping (current: ${order.status})` + }); + } + + order.status = 'shipped'; + order.trackingNumber = trackingNumber; + order.courier = courier; + + await order.save(); + + res.status(200).json({ + status: 'success', + message: 'Order marked as shipped', + data: order + }); + + } catch (error) { + next(error); + } +}; +module.exports = { placeOrder,getMyOrders, getShopOrders,confirmOrder,shipOrder }; diff --git a/modules/orders/order.routes.js b/modules/orders/order.routes.js index 18ecc96..9d2ed30 100644 --- a/modules/orders/order.routes.js +++ b/modules/orders/order.routes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole,isShopOwner } = require('../../middleware/auth'); -const { placeOrder,getMyOrders,getShopOrders,confirmOrder } = require('./order.controller'); +const { placeOrder,getMyOrders,getShopOrders,confirmOrder,shipOrder } = require('./order.controller'); // Middleware: verifyToken, requireRole, isOrderOwner // POST /api/orders — buyer: place order (snapshot items, compute total) @@ -53,6 +53,11 @@ router.put('/:id/confirm',verifyToken,requireRole('seller'),confirmOrder); // GET http://localhost:5000/api/order/shops/6a3213b4152aaded1ac9a0ff/orders - +// PUT /api/orders/:id/ship — seller: confirmed → shipped (body: { trackingNumber, courier }) +router.put('/:id/ship',verifyToken,requireRole('seller'),shipOrder); +// example payoad { +// "trackingNumber": "LBC123456789", +// "courier": "LBC" +// } module.exports = router; \ No newline at end of file From 885b4e23e4b2fa909d13c450e007b3d4cf1ff1d4 Mon Sep 17 00:00:00 2001 From: Yassef Date: Thu, 18 Jun 2026 18:52:41 +0800 Subject: [PATCH 28/37] feat: shipped to delivered --- modules/orders/order.controller.js | 70 +++++++++++++++++++++++++++++- modules/orders/order.routes.js | 7 ++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/modules/orders/order.controller.js b/modules/orders/order.controller.js index f875de7..ccc2c38 100644 --- a/modules/orders/order.controller.js +++ b/modules/orders/order.controller.js @@ -11,6 +11,9 @@ const Order = require('./order.model'); const Product = require('../products/product.model'); + + + const placeOrder = async (req, res, next) => { try { const { @@ -78,6 +81,11 @@ const placeOrder = async (req, res, next) => { next(error); } }; + + + + + const getMyOrders = async (req, res, next) => { try { @@ -97,6 +105,11 @@ const getMyOrders = async (req, res, next) => { next(error); } }; + + + + + const getShopOrders = async (req, res, next) => { try { const { id } = req.params; @@ -119,6 +132,10 @@ const getShopOrders = async (req, res, next) => { }; + + + + const confirmOrder = async (req, res, next) => { try { const { id } = req.params; @@ -161,6 +178,11 @@ const confirmOrder = async (req, res, next) => { next(error); } }; + + + + + const shipOrder = async (req, res, next) => { try { const { id } = req.params; @@ -214,4 +236,50 @@ const shipOrder = async (req, res, next) => { next(error); } }; -module.exports = { placeOrder,getMyOrders, getShopOrders,confirmOrder,shipOrder }; + +const deliverOrder = async (req, res, next) => { + try { + const { id } = req.params; + + const order = await Order.findById(id).populate('shop'); + + if (!order) { + return res.status(404).json({ + status: 'error', + message: 'Order not found' + }); + } + + // check ownership (seller must own shop) + if (order.shop.owner.toString() !== req.user.id) { + return res.status(403).json({ + status: 'error', + message: 'Not authorized to deliver this order' + }); + } + + // validate current status + if (order.status !== 'shipped') { + return res.status(400).json({ + status: 'error', + message: `Order must be shipped before delivery (current: ${order.status})` + }); + } + + order.status = 'delivered'; + + await order.save(); + + res.status(200).json({ + status: 'success', + message: 'Order marked as delivered', + data: order + }); + + } catch (error) { + next(error); + } +}; + + +module.exports = { placeOrder,getMyOrders, getShopOrders,confirmOrder,shipOrder,deliverOrder }; diff --git a/modules/orders/order.routes.js b/modules/orders/order.routes.js index 9d2ed30..3cdad97 100644 --- a/modules/orders/order.routes.js +++ b/modules/orders/order.routes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole,isShopOwner } = require('../../middleware/auth'); -const { placeOrder,getMyOrders,getShopOrders,confirmOrder,shipOrder } = require('./order.controller'); +const { placeOrder,getMyOrders,getShopOrders,confirmOrder,shipOrder,deliverOrder } = require('./order.controller'); // Middleware: verifyToken, requireRole, isOrderOwner // POST /api/orders — buyer: place order (snapshot items, compute total) @@ -60,4 +60,9 @@ router.put('/:id/ship',verifyToken,requireRole('seller'),shipOrder); // "courier": "LBC" // } +// PUT /api/orders/:id/deliver — seller: shipped → delivered +router.put('/:id/deliver',verifyToken,requireRole('seller'),deliverOrder); + + + module.exports = router; \ No newline at end of file From 69614d2143f5ee294be23288fe41095d2f5a93e8 Mon Sep 17 00:00:00 2001 From: Yassef Date: Thu, 18 Jun 2026 19:09:59 +0800 Subject: [PATCH 29/37] feat: cancel order --- modules/orders/order.controller.js | 53 +++++++++++++++++++++++++++++- modules/orders/order.routes.js | 8 ++++- server.js | 2 +- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/modules/orders/order.controller.js b/modules/orders/order.controller.js index ccc2c38..8593df3 100644 --- a/modules/orders/order.controller.js +++ b/modules/orders/order.controller.js @@ -280,6 +280,57 @@ const deliverOrder = async (req, res, next) => { next(error); } }; +const cancelOrder = async (req, res, next) => { + try { + const { id } = req.params; + + const order = await Order.findById(id).populate('shop'); + + if (!order) { + return res.status(404).json({ + status: 'error', + message: 'Order not found' + }); + } + + const isBuyer = order.buyer.toString() === req.user.id; + const isSeller = order.shop.owner.toString() === req.user.id; + + if (!isBuyer && !isSeller) { + return res.status(403).json({ + status: 'error', + message: 'Not authorized to cancel this order' + }); + } + // prevent cancel after shipping + if (order.status === 'shipped' || order.status === 'delivered') { + return res.status(400).json({ + status: 'error', + message: `Cannot cancel order that is already ${order.status}` + }); + } + + if (order.status === 'cancelled') { + return res.status(400).json({ + status: 'error', + message: 'Order is already cancelled' + }); + } + + order.status = 'cancelled'; + + await order.save(); + + res.status(200).json({ + status: 'success', + message: 'Order cancelled successfully', + data: order + }); + + } catch (error) { + next(error); + } +}; -module.exports = { placeOrder,getMyOrders, getShopOrders,confirmOrder,shipOrder,deliverOrder }; +module.exports = { placeOrder,getMyOrders, getShopOrders,confirmOrder,shipOrder,deliverOrder,cancelOrder }; diff --git a/modules/orders/order.routes.js b/modules/orders/order.routes.js index 3cdad97..16695e0 100644 --- a/modules/orders/order.routes.js +++ b/modules/orders/order.routes.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole,isShopOwner } = require('../../middleware/auth'); -const { placeOrder,getMyOrders,getShopOrders,confirmOrder,shipOrder,deliverOrder } = require('./order.controller'); +const { placeOrder,getMyOrders,getShopOrders,confirmOrder,shipOrder,deliverOrder,cancelOrder } = require('./order.controller'); // Middleware: verifyToken, requireRole, isOrderOwner // POST /api/orders — buyer: place order (snapshot items, compute total) @@ -65,4 +65,10 @@ router.put('/:id/deliver',verifyToken,requireRole('seller'),deliverOrder); +// PUT /api/orders/:id/cancel — buyer or seller: cancel (only pre-shipped) +router.put('/:id/cancel', verifyToken, cancelOrder); +// exampel http://localhost:5000/api/orders/6a32c0167379408d95ee4e8f/cancel + + + module.exports = router; \ No newline at end of file diff --git a/server.js b/server.js index 5262888..2f5aa72 100644 --- a/server.js +++ b/server.js @@ -28,7 +28,7 @@ app.get('/api/health', (req, res) => { // app.use('/api/auth', require('./modules/auth/routes')); // app.use('/api/payments', require('./modules/payments/routes')); -app.use('/api/order', require('./modules/orders/order.routes')); +app.use('/api/orders', require('./modules/orders/order.routes')); app.use('/api/shops', require('./modules/shops/shop.routes')); app.use('/api/users', require('./modules/users/user.routes')); From 9c0124a3d7fb61e569e43d8c40d560cdf03c2bba Mon Sep 17 00:00:00 2001 From: Yassef Date: Fri, 19 Jun 2026 00:39:53 +0800 Subject: [PATCH 30/37] chore: updated the create checkout and paymongo sevices --- modules/payments/payment.controller.js | 67 ++++++++++---------------- modules/products/product.routes.js | 2 +- modules/shops/shop.routes.js | 14 ++++++ server.js | 2 +- services/paymongo.js | 4 ++ 5 files changed, 45 insertions(+), 44 deletions(-) diff --git a/modules/payments/payment.controller.js b/modules/payments/payment.controller.js index b2b93ce..d18d4c4 100644 --- a/modules/payments/payment.controller.js +++ b/modules/payments/payment.controller.js @@ -1,5 +1,6 @@ const Payment = require('./payment.model'); const Order = require('../orders/order.model'); +const User = require('../users/user.model'); const paymongo = require('../../services/paymongo'); const { checkout } = require('./payment.routes'); @@ -41,47 +42,60 @@ const createCheckout = async (req, res, next) => { }); } - // 3. Convert to centavos (your schema already uses centavos) - const amount = order.total; + // 3. Get user details (IMPORTANT FIX) + const user = await User.findById(req.user.id); + + if (!user) { + return res.status(404).json({ + status: 'error', + message: 'User not found' + }); + } // 4. Create PayMongo checkout session const response = await paymongo.post('/checkout_sessions', { data: { attributes: { billing: { - name: req.user.name, - email: req.user.email + name: user.name, + email: user.email }, + line_items: order.items.map(item => ({ name: item.name, quantity: item.quantity, - amount: item.price, // already centavos + amount: item.price, currency: 'PHP' })), + payment_method_types: ['gcash', 'card'], + description: `Payment for Order ${order._id}`, + success_url: 'http://localhost:3000/success', cancel_url: 'http://localhost:3000/cancel' } } }); - const checkoutUrl = response.data.data.attributes.checkout_url; - const linkId = response.data.data.id; + + const checkoutSession = response.data.data; + const checkoutUrl = checkoutSession.attributes.checkout_url; + const linkId = checkoutSession.id; - // 5. Save payment record + const payment = await Payment.create({ order: order._id, buyer: req.user.id, paymongoLinkId: linkId, checkoutUrl, - amount, + amount: order.total, status: 'pending' }); - res.status(201).json({ + return res.status(201).json({ status: 'success', - message: 'Payment link created successfully', + message: 'Checkout session created successfully', data: { checkoutUrl, payment @@ -93,38 +107,7 @@ const createCheckout = async (req, res, next) => { } }; -const stripeWebhook = async (req, res) => { - // PayMongo uses webhooks, not Stripe naming - const event = req.body; - try { - if (event.data.attributes.type === 'checkout_session.payment.paid') { - - const linkId = event.data.attributes.data.id; - - const payment = await Payment.findOne({ - paymongoLinkId: linkId - }); - - if (!payment) return res.sendStatus(404); - - payment.status = 'paid'; - payment.paidAt = new Date(); - payment.webhookPayload = event; - - await payment.save(); - - await Order.findByIdAndUpdate(payment.order, { - status: 'confirmed' - }); - } - - res.sendStatus(200); - - } catch (err) { - res.sendStatus(500); - } -}; const getPaymentStatus = async (req, res, next) => { try { diff --git a/modules/products/product.routes.js b/modules/products/product.routes.js index d899492..b21330f 100644 --- a/modules/products/product.routes.js +++ b/modules/products/product.routes.js @@ -35,7 +35,7 @@ router.post('/shops/:shopId/products',verifyToken ,requireRole('seller'), upload // "description": "Handmade premium wooden chair made from solid mahogany wood", // "price": 2500, // "category": "Furniture", -// "imageUrl": "https://res.cloudinary.com/demo/image/upload/sample.jpg", (hindi gagana if raw json) +// "image": "https://res.cloudinary.com/demo/image/upload/sample.jpg", (hindi gagana if raw json) // "colors": ["Brown", "Dark Brown"], // "sizes": ["Small", "Medium"], // "stockQuantity": 10 diff --git a/modules/shops/shop.routes.js b/modules/shops/shop.routes.js index 0e85758..405d065 100644 --- a/modules/shops/shop.routes.js +++ b/modules/shops/shop.routes.js @@ -35,8 +35,22 @@ router.get('/nearby', getNearbyShops); // example http://localhost:5000/api/shops/6a3213b4152aaded1ac9a0ff router.get('/:id', getShopById); + + + + // POST /api/shops — seller: create shop with coords → pin appears on map router.post('/',verifyToken,requireRole('seller'),upload.fields([{ name: 'cover', maxCount: 1 },{ name: 'logo', maxCount: 1 }]),createShop); +// payload +// name: Juan's Crafts Shop +// description: Handmade wooden products +// category: Handicrafts +// lat: 8.228 +// lng: 124.246 +// address: Marawi City +// cover: (file upload) +// logo: (file upload) + // PUT /api/shops/:id — seller (isShopOwner): update shop info // EXAMPLE http://localhost:5000/api/shops/6a321e72c43e01b6215a55ad diff --git a/server.js b/server.js index 2f5aa72..971b6a2 100644 --- a/server.js +++ b/server.js @@ -26,7 +26,7 @@ app.get('/api/health', (req, res) => { // TODO: Import and use your module routes here // app.use('/api/auth', require('./modules/auth/routes')); -// app.use('/api/payments', require('./modules/payments/routes')); +app.use('/api/payments', require('./modules/payments/payment.routes')); app.use('/api/orders', require('./modules/orders/order.routes')); app.use('/api/shops', require('./modules/shops/shop.routes')); diff --git a/services/paymongo.js b/services/paymongo.js index 1ad3175..21bd1af 100644 --- a/services/paymongo.js +++ b/services/paymongo.js @@ -2,6 +2,10 @@ const axios = require('axios'); const PAYMONGO_SECRET = process.env.PAYMONGO_SECRET_KEY; +if (!PAYMONGO_SECRET) { + console.error('❌ PAYMONGO_SECRET_KEY is missing'); +} + const paymongo = axios.create({ baseURL: 'https://api.paymongo.com/v1', headers: { From cb38316218412a613578496db31721983b64e989 Mon Sep 17 00:00:00 2001 From: Yassef Date: Fri, 19 Jun 2026 01:17:37 +0800 Subject: [PATCH 31/37] Chore: final update for paymongo configuration --- modules/payments/payment.controller.js | 58 +++++++++++++++++--------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/modules/payments/payment.controller.js b/modules/payments/payment.controller.js index d18d4c4..2761337 100644 --- a/modules/payments/payment.controller.js +++ b/modules/payments/payment.controller.js @@ -2,17 +2,11 @@ const Payment = require('./payment.model'); const Order = require('../orders/order.model'); const User = require('../users/user.model'); const paymongo = require('../../services/paymongo'); -const { checkout } = require('./payment.routes'); -// TODO: Implement payments controller -// Planned handlers: -// createCheckout — create a Stripe or PayMongo checkout session for an order -// stripeWebhook — handle Stripe webhook events (payment_intent.succeeded, etc.) -// paymongoWebhook — handle PayMongo webhook events -// getPaymentStatus — return payment status for a given order -// refundPayment — admin: trigger a refund via gateway API - +// ───────────────────────────────────────────── +// CREATE CHECKOUT SESSION (PayMongo) +// ───────────────────────────────────────────── const createCheckout = async (req, res, next) => { try { const { orderId } = req.body; @@ -42,7 +36,7 @@ const createCheckout = async (req, res, next) => { }); } - // 3. Get user details (IMPORTANT FIX) + // 3. Get user const user = await User.findById(req.user.id); if (!user) { @@ -64,7 +58,7 @@ const createCheckout = async (req, res, next) => { line_items: order.items.map(item => ({ name: item.name, quantity: item.quantity, - amount: item.price, + amount: Math.round(item.price), // already centavos currency: 'PHP' })), @@ -78,12 +72,12 @@ const createCheckout = async (req, res, next) => { } }); - const checkoutSession = response.data.data; + const checkoutUrl = checkoutSession.attributes.checkout_url; const linkId = checkoutSession.id; - + // 5. Save payment record const payment = await Payment.create({ order: order._id, buyer: req.user.id, @@ -108,7 +102,9 @@ const createCheckout = async (req, res, next) => { }; - +// ───────────────────────────────────────────── +// GET PAYMENT STATUS +// ───────────────────────────────────────────── const getPaymentStatus = async (req, res, next) => { try { const { orderId } = req.params; @@ -131,15 +127,28 @@ const getPaymentStatus = async (req, res, next) => { next(error); } }; + + +// ───────────────────────────────────────────── +// PAYMONGO WEBHOOK (SECURE + SAFE) +// ───────────────────────────────────────────── const paymongoWebhook = async (req, res) => { try { const event = req.body; - const eventType = event.data.attributes.type; + const eventType = event?.data?.attributes?.type; if (eventType === 'checkout_session.payment.paid') { - const linkId = event.data.attributes.data.id; + const sessionData = event.data.attributes.data; + + const linkId = + sessionData?.id || + sessionData?.attributes?.id; + + if (!linkId) { + return res.sendStatus(400); + } const payment = await Payment.findOne({ paymongoLinkId: linkId @@ -147,28 +156,37 @@ const paymongoWebhook = async (req, res) => { if (!payment) return res.sendStatus(404); + + if (payment.status === 'paid') { + return res.sendStatus(200); + } + payment.status = 'paid'; payment.paidAt = new Date(); payment.webhookPayload = event; await payment.save(); - // update order + // update order only once await Order.findByIdAndUpdate(payment.order, { status: 'confirmed' }); } - res.sendStatus(200); + return res.sendStatus(200); } catch (error) { console.error(error); - res.sendStatus(500); + return res.sendStatus(500); } }; + +// ───────────────────────────────────────────── +// EXPORTS +// ───────────────────────────────────────────── module.exports = { createCheckout, getPaymentStatus, paymongoWebhook -}; +}; \ No newline at end of file From 9617105c98b9b85f5371b0d31b4dff4347ec2470 Mon Sep 17 00:00:00 2001 From: Yassef Date: Fri, 19 Jun 2026 02:40:05 +0800 Subject: [PATCH 32/37] feat: dashboard: total users, shops, orders, revenue --- modules/admin/admin.controller.js | 45 ++++++++++++++++++++++++++++++- modules/admin/admin.routes.js | 7 +++++ server.js | 2 +- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/modules/admin/admin.controller.js b/modules/admin/admin.controller.js index ae2ae8b..5e11abd 100644 --- a/modules/admin/admin.controller.js +++ b/modules/admin/admin.controller.js @@ -1,3 +1,8 @@ +const User = require('../users/user.model'); +const Shop = require('../shops/shop.model'); +const Order = require('../orders/order.model'); + + // TODO: Implement admin controller // Planned handlers: // getStats — aggregate: total users, sellers, active shops, total orders, gross revenue @@ -7,4 +12,42 @@ // toggleShop — flip shop.isActive (removes from / restores to map) // getAllOrders — read-only paginated order ledger across all buyers/sellers -module.exports = {}; +const getStats = async (req, res, next) => { + try { + // 1. TOTAL USERS + const users = await User.countDocuments(); + + // 2. TOTAL SHOPS + const shops = await Shop.countDocuments(); + + // 3. TOTAL ORDERS + const orders = await Order.countDocuments(); + + // 4. REVENUE (only delivered orders) + const deliveredOrders = await Order.find({ + status: 'delivered' + }); + + const revenue = deliveredOrders.reduce((sum, order) => { + return sum + (order.total || 0); + }, 0); + + // 5. RESPONSE + res.status(200).json({ + status: 'success', + data: { + users, + shops, + orders, + revenue + } + }); + + } catch (error) { + next(error); + } +}; + +module.exports = { + getStats +}; diff --git a/modules/admin/admin.routes.js b/modules/admin/admin.routes.js index 8fe58bf..dcc9076 100644 --- a/modules/admin/admin.routes.js +++ b/modules/admin/admin.routes.js @@ -2,6 +2,8 @@ const express = require('express'); const router = express.Router(); +const { verifyToken, requireRole } = require('../../middleware/auth'); +const { getStats } = require('./admin.controller'); // All routes require: verifyToken + requireRole('admin') // GET /api/admin/stats — dashboard: total users, shops, orders, revenue @@ -11,4 +13,9 @@ const router = express.Router(); // PUT /api/admin/shops/:id/toggle — toggle shop isActive (show / hide from map) // GET /api/admin/orders — read-only ledger of all orders + +// GET /api/admin/stats — dashboard: total users, shops, orders, revenue +router.get('/stats',verifyToken,requireRole('admin'),getStats); + + module.exports = router; diff --git a/server.js b/server.js index 971b6a2..0dff586 100644 --- a/server.js +++ b/server.js @@ -25,7 +25,7 @@ app.get('/api/health', (req, res) => { }); // TODO: Import and use your module routes here -// app.use('/api/auth', require('./modules/auth/routes')); +app.use('/api/admin', require('./modules/admin/admin.routes')); app.use('/api/payments', require('./modules/payments/payment.routes')); app.use('/api/orders', require('./modules/orders/order.routes')); From d9583c87e128d303684c64a51a4031757a8f87c3 Mon Sep 17 00:00:00 2001 From: Yassef Date: Fri, 19 Jun 2026 02:44:44 +0800 Subject: [PATCH 33/37] feat: list all users under admin --- modules/admin/admin.controller.js | 20 +++++++++++++++++++- modules/admin/admin.routes.js | 4 +++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/modules/admin/admin.controller.js b/modules/admin/admin.controller.js index 5e11abd..6a74cf3 100644 --- a/modules/admin/admin.controller.js +++ b/modules/admin/admin.controller.js @@ -48,6 +48,24 @@ const getStats = async (req, res, next) => { } }; +const getUsers = async (req, res, next) => { + try { + const users = await User.find() + .select('-password') + .sort({ createdAt: -1 }); + + res.status(200).json({ + status: 'success', + count: users.length, + data: users + }); + + } catch (error) { + next(error); + } +}; + module.exports = { - getStats + getStats, + getUsers }; diff --git a/modules/admin/admin.routes.js b/modules/admin/admin.routes.js index dcc9076..d6454e9 100644 --- a/modules/admin/admin.routes.js +++ b/modules/admin/admin.routes.js @@ -3,7 +3,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole } = require('../../middleware/auth'); -const { getStats } = require('./admin.controller'); +const { getStats,getUsers } = require('./admin.controller'); // All routes require: verifyToken + requireRole('admin') // GET /api/admin/stats — dashboard: total users, shops, orders, revenue @@ -17,5 +17,7 @@ const { getStats } = require('./admin.controller'); // GET /api/admin/stats — dashboard: total users, shops, orders, revenue router.get('/stats',verifyToken,requireRole('admin'),getStats); +// GET /api/admin/users — list all users +router.get('/users',verifyToken,requireRole('admin'),getUsers); module.exports = router; From e40b0ee3e228a4a1a8227ecc59a07ddbc2304876 Mon Sep 17 00:00:00 2001 From: Yassef Date: Fri, 19 Jun 2026 02:49:52 +0800 Subject: [PATCH 34/37] feat:toggle user isActive (suspend / unsuspend) --- modules/admin/admin.controller.js | 34 ++++++++++++++++++++++++++++++- modules/admin/admin.routes.js | 4 +++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/modules/admin/admin.controller.js b/modules/admin/admin.controller.js index 6a74cf3..4fecd74 100644 --- a/modules/admin/admin.controller.js +++ b/modules/admin/admin.controller.js @@ -65,7 +65,39 @@ const getUsers = async (req, res, next) => { } }; +const toggleUser = async (req, res, next) => { + try { + const { id } = req.params; + + const user = await User.findById(id); + + if (!user) { + return res.status(404).json({ + status: 'error', + message: 'User not found' + }); + } + + // toggle status + user.isActive = !user.isActive; + + await user.save(); + + res.status(200).json({ + status: 'success', + message: `User has been ${user.isActive ? 'activated' : 'suspended'}`, + data: { + id: user._id, + isActive: user.isActive + } + }); + + } catch (error) { + next(error); + } +}; module.exports = { getStats, - getUsers + getUsers, + toggleUser }; diff --git a/modules/admin/admin.routes.js b/modules/admin/admin.routes.js index d6454e9..2d41d36 100644 --- a/modules/admin/admin.routes.js +++ b/modules/admin/admin.routes.js @@ -3,7 +3,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole } = require('../../middleware/auth'); -const { getStats,getUsers } = require('./admin.controller'); +const { getStats,getUsers,toggleUser } = require('./admin.controller'); // All routes require: verifyToken + requireRole('admin') // GET /api/admin/stats — dashboard: total users, shops, orders, revenue @@ -20,4 +20,6 @@ router.get('/stats',verifyToken,requireRole('admin'),getStats); // GET /api/admin/users — list all users router.get('/users',verifyToken,requireRole('admin'),getUsers); +// PUT /api/admin/users/:id/toggle — toggle user isActive (suspend / unsuspend) +router.put('/users/:id/toggle',verifyToken,requireRole('admin'),toggleUser); module.exports = router; From d18426aa91a2eb127a83247a80f0b2a003c24918 Mon Sep 17 00:00:00 2001 From: Yassef Date: Fri, 19 Jun 2026 02:54:01 +0800 Subject: [PATCH 35/37] feat: list all shops under admin --- modules/admin/admin.controller.js | 21 ++++++++++++++++++++- modules/admin/admin.routes.js | 5 ++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/modules/admin/admin.controller.js b/modules/admin/admin.controller.js index 4fecd74..a221787 100644 --- a/modules/admin/admin.controller.js +++ b/modules/admin/admin.controller.js @@ -96,8 +96,27 @@ const toggleUser = async (req, res, next) => { next(error); } }; + +const getShops = async (req, res, next) => { + try { + const shops = await Shop.find() + .populate('owner', 'name email role') // show who owns the shop + .sort({ createdAt: -1 }); + + res.status(200).json({ + status: 'success', + count: shops.length, + data: shops + }); + + } catch (error) { + next(error); + } +}; + module.exports = { getStats, getUsers, - toggleUser + toggleUser, + getShops }; diff --git a/modules/admin/admin.routes.js b/modules/admin/admin.routes.js index 2d41d36..0c81ec8 100644 --- a/modules/admin/admin.routes.js +++ b/modules/admin/admin.routes.js @@ -3,7 +3,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole } = require('../../middleware/auth'); -const { getStats,getUsers,toggleUser } = require('./admin.controller'); +const { getStats,getUsers,toggleUser,getShops } = require('./admin.controller'); // All routes require: verifyToken + requireRole('admin') // GET /api/admin/stats — dashboard: total users, shops, orders, revenue @@ -22,4 +22,7 @@ router.get('/users',verifyToken,requireRole('admin'),getUsers); // PUT /api/admin/users/:id/toggle — toggle user isActive (suspend / unsuspend) router.put('/users/:id/toggle',verifyToken,requireRole('admin'),toggleUser); + +// GET /api/admin/shops — list all shops +router.get('/shops',verifyToken,requireRole('admin'),getShops); module.exports = router; From 657a0c83184a31d51d3ff00d20e7419f6bc372cc Mon Sep 17 00:00:00 2001 From: Yassef Date: Fri, 19 Jun 2026 02:58:18 +0800 Subject: [PATCH 36/37] feat: toggle shop isActive under Admin --- modules/admin/admin.controller.js | 34 ++++++++++++++++++++++++++++++- modules/admin/admin.routes.js | 5 ++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/modules/admin/admin.controller.js b/modules/admin/admin.controller.js index a221787..1dd803d 100644 --- a/modules/admin/admin.controller.js +++ b/modules/admin/admin.controller.js @@ -114,9 +114,41 @@ const getShops = async (req, res, next) => { } }; +const toggleShop = async (req, res, next) => { + try { + const { id } = req.params; + + const shop = await Shop.findById(id); + + if (!shop) { + return res.status(404).json({ + status: 'error', + message: 'Shop not found' + }); + } + + // toggle status + shop.isActive = !shop.isActive; + + await shop.save(); + + res.status(200).json({ + status: 'success', + message: `Shop has been ${shop.isActive ? 'activated' : 'hidden'}`, + data: { + id: shop._id, + isActive: shop.isActive + } + }); + + } catch (error) { + next(error); + } +}; module.exports = { getStats, getUsers, toggleUser, - getShops + getShops, + toggleShop }; diff --git a/modules/admin/admin.routes.js b/modules/admin/admin.routes.js index 0c81ec8..928a57a 100644 --- a/modules/admin/admin.routes.js +++ b/modules/admin/admin.routes.js @@ -3,7 +3,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole } = require('../../middleware/auth'); -const { getStats,getUsers,toggleUser,getShops } = require('./admin.controller'); +const { getStats,getUsers,toggleUser,getShops,toggleShop } = require('./admin.controller'); // All routes require: verifyToken + requireRole('admin') // GET /api/admin/stats — dashboard: total users, shops, orders, revenue @@ -25,4 +25,7 @@ router.put('/users/:id/toggle',verifyToken,requireRole('admin'),toggleUser); // GET /api/admin/shops — list all shops router.get('/shops',verifyToken,requireRole('admin'),getShops); + +// PUT /api/admin/shops/:id/toggle — toggle shop isActive (show / hide from map) +router.put('/shops/:id/toggle',verifyToken,requireRole('admin'),toggleShop); module.exports = router; From fdab61553df7c65840a719da0310bd8ca578a20c Mon Sep 17 00:00:00 2001 From: Yassef Date: Fri, 19 Jun 2026 03:03:24 +0800 Subject: [PATCH 37/37] feat: read-only ledger of all orders under admin --- modules/admin/admin.controller.js | 21 ++++++++++++++++++++- modules/admin/admin.routes.js | 7 ++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/modules/admin/admin.controller.js b/modules/admin/admin.controller.js index 1dd803d..70a92e8 100644 --- a/modules/admin/admin.controller.js +++ b/modules/admin/admin.controller.js @@ -145,10 +145,29 @@ const toggleShop = async (req, res, next) => { next(error); } }; + +const getOrders = async (req, res, next) => { + try { + const orders = await Order.find() + .populate('buyer', 'name email') + .populate('shop', 'name owner') + .sort({ createdAt: -1 }); + + res.status(200).json({ + status: 'success', + count: orders.length, + data: orders + }); + + } catch (error) { + next(error); + } +}; module.exports = { getStats, getUsers, toggleUser, getShops, - toggleShop + toggleShop, + getOrders }; diff --git a/modules/admin/admin.routes.js b/modules/admin/admin.routes.js index 928a57a..df41408 100644 --- a/modules/admin/admin.routes.js +++ b/modules/admin/admin.routes.js @@ -3,7 +3,7 @@ const express = require('express'); const router = express.Router(); const { verifyToken, requireRole } = require('../../middleware/auth'); -const { getStats,getUsers,toggleUser,getShops,toggleShop } = require('./admin.controller'); +const { getStats,getUsers,toggleUser,getShops,toggleShop,getOrders } = require('./admin.controller'); // All routes require: verifyToken + requireRole('admin') // GET /api/admin/stats — dashboard: total users, shops, orders, revenue @@ -28,4 +28,9 @@ router.get('/shops',verifyToken,requireRole('admin'),getShops); // PUT /api/admin/shops/:id/toggle — toggle shop isActive (show / hide from map) router.put('/shops/:id/toggle',verifyToken,requireRole('admin'),toggleShop); + +// GET /api/admin/orders — read-only ledger of all orders +router.get('/orders',verifyToken,requireRole('admin'),getOrders); + + module.exports = router;