feat: implement customer profile management and business application features

- Added customer profile controller with endpoints for retrieving, updating, and deactivating user profiles.
- Introduced address model and routes for managing user addresses.
- Created business application model and service for handling business applications, including approval and rejection processes.
- Developed business contact and credit account models to support business customer functionalities.
- Implemented settlement term model for managing payment terms.
- Added email templates for business application notifications (approved, rejected, received, and status changes).
- Enhanced validation schemas for customer and business inputs to ensure data integrity.
- Created unit tests for business application service and validation schemas to ensure functionality and correctness.
- Added migration scripts to set up new database tables and columns for customer and business features.
This commit is contained in:
Sathira Sri Sathara
2026-09-03 14:58:26 +05:30
parent b6b345f245
commit fd4e324571
36 changed files with 406 additions and 6 deletions
@@ -0,0 +1,7 @@
const crypto=require("crypto"); const {Op}=require("sequelize"); const db=require("../../models"); const {nextSequence}=require("../../utils/referenceNumber.util");
const { revokeAllUserSessions } = require("../auth/session.service");
const normalize=(b)=>({business_name:b.businessName,legal_name:b.legalName,registration_number:b.registrationNumber,tax_number:b.taxNumber,business_type:b.businessType,contact_email:b.contactEmail,contact_phone:b.contactPhone,website:b.website});
const apply=async(userId,body)=>db.sequelize.transaction(async t=>{const user=await db.User.findByPk(userId,{transaction:t,lock:t.LOCK.UPDATE});if(!user||user.accountStatus!=="ACTIVE"||!user.emailVerifiedAt||user.accountType!=="customer")throw Object.assign(new Error("Only verified active customers may apply"),{status:403,code:"NOT_ELIGIBLE"});const active=await db.BusinessApplication.findOne({where:{user_id:userId,status:{[Op.in]:["PENDING","UNDER_REVIEW"]}},transaction:t,lock:t.LOCK.UPDATE});if(active)throw Object.assign(new Error("An active application already exists"),{status:409,code:"ACTIVE_APPLICATION_EXISTS"});return db.BusinessApplication.create({id:crypto.randomUUID(),user_id:userId,...normalize(body),status:"PENDING"},{transaction:t});});
const approve=async(id,reviewerId)=>db.sequelize.transaction(async t=>{const application=await db.BusinessApplication.findByPk(id,{transaction:t,lock:t.LOCK.UPDATE});if(!application)throw Object.assign(new Error("Application not found"),{status:404,code:"NOT_FOUND"});if(!["PENDING","UNDER_REVIEW"].includes(application.status))throw Object.assign(new Error("Application cannot be approved from its current state"),{status:409,code:"INVALID_TRANSITION"});const user=await db.User.findByPk(application.user_id,{transaction:t,lock:t.LOCK.UPDATE});const existing=await db.BusinessCustomer.findOne({where:{user_id:application.user_id},transaction:t,lock:t.LOCK.UPDATE});if(existing)throw Object.assign(new Error("Business profile already exists"),{status:409,code:"BUSINESS_EXISTS"});const number=await nextSequence("business_partner",t);const partnerId=`ZUM-BIZ-${String(number).padStart(6,"0")}`;const profile=await db.BusinessCustomer.create({business_customer_id:`biz_${crypto.randomUUID()}`,user_id:application.user_id,businessName:application.business_name,legalName:application.legal_name,businessRegistrationNumber:application.registration_number,taxNumber:application.tax_number,businessType:application.business_type,contactName:application.business_name,phoneNumber:application.contact_phone,businessEmail:application.contact_email,website:application.website,expectedMonthlyVolume:"NOT_CONFIGURED",partnerId,status:"ACTIVE",approvedAt:new Date(),approvedBy:reviewerId},{transaction:t});await db.BusinessCreditAccount.create({id:crypto.randomUUID(),business_profile_id:profile.business_customer_id,currency:"LKR",credit_limit:0,status:"DISABLED"},{transaction:t});await user.update({accountType:"business_customer",tokenVersion:user.tokenVersion+1},{transaction:t});await revokeAllUserSessions(user.id,"BUSINESS_APPLICATION_APPROVED",t);await application.update({status:"APPROVED",reviewed_by:reviewerId,reviewed_at:new Date(),rejection_reason:null},{transaction:t});return {application,profile};});
const reject=async(id,reviewerId,reason)=>db.sequelize.transaction(async t=>{const application=await db.BusinessApplication.findByPk(id,{transaction:t,lock:t.LOCK.UPDATE});if(!application)throw Object.assign(new Error("Application not found"),{status:404,code:"NOT_FOUND"});if(!["PENDING","UNDER_REVIEW"].includes(application.status))throw Object.assign(new Error("Application cannot be rejected from its current state"),{status:409,code:"INVALID_TRANSITION"});await application.update({status:"REJECTED",reviewed_by:reviewerId,reviewed_at:new Date(),rejection_reason:reason},{transaction:t});return application;});
module.exports={apply,approve,reject,normalize};
+1
View File
@@ -4,6 +4,7 @@ const db = require("../../models");
const templates = {
otp: { subject: "Your ZUMRI login code" }, passwordChanged: { subject: "Your ZUMRI password was changed" },
passwordReset: { subject: "Reset your ZUMRI password" }, emailVerification: { subject: "Verify your ZUMRI email" }, welcome: { subject: "Welcome to ZUMRI" },
businessApplicationReceived: { subject: "Your ZUMRI business application was received" }, businessApplicationApproved: { subject: "Your ZUMRI business application was approved" }, businessApplicationRejected: { subject: "Your ZUMRI business application was reviewed" }, businessAccountStatusChanged: { subject: "Your ZUMRI business account status changed" },
};
const send = async ({ templateKey, recipient, locale = "en", variables = {}, correlationId, notificationId, userId, eventId }) => {
if (!templates[templateKey]) throw new Error("Unknown email template");
@@ -0,0 +1,2 @@
const crypto=require("crypto"); const db=require("../../models"); const email=require("../email/email.service");
const publish=async({type,user,headline,description,templateKey,variables={},correlationId})=>{const notification=await db.notification.create({notification_id:`notif_${crypto.randomUUID()}`,notificationHeadline:headline,notificationDescription:description,notificationType:"USER",dateCreated:new Date()});await db.userNotification.create({user_id:user.id,notification_id:notification.notification_id});if(templateKey)await email.send({templateKey,recipient:user.email,userId:user.id,notificationId:notification.notification_id,variables,correlationId,eventId:`${type}-${notification.notification_id}`});return notification;};module.exports={publish};