Compare commits
16 Commits
split-serv
...
e149884f72
| Author | SHA1 | Date | |
|---|---|---|---|
| e149884f72 | |||
| a31ec97640 | |||
| 0c97412057 | |||
|
|
b4ff39c0d7 | ||
| bb5da7647b | |||
| 3f19bb4087 | |||
| be8b9cef7d | |||
|
|
77cef98091 | ||
| 97f9c2b26e | |||
|
|
b93cd6b32c | ||
|
|
51319a69fc | ||
| 5ad46309ef | |||
|
|
781212277a | ||
| 6b0ee461c5 | |||
| cc2fa3eb6b | |||
| fe6bb59cc7 |
@@ -1,40 +0,0 @@
|
||||
# Split Serverless services (deploy order)
|
||||
|
||||
This repo is split into multiple Serverless configs so you can deploy smaller CloudFormation stacks instead of one huge stack.
|
||||
|
||||
## Config files
|
||||
|
||||
- `serverless.layers.yml`: Prisma layer stack (deploy once per stage)
|
||||
- `serverless.host.yml`: Host + PQQ functions (owns the shared HTTP API)
|
||||
- `serverless.admin.yml`: Minglar Admin functions (attaches routes to Host HTTP API)
|
||||
- `serverless.user.yml`: User functions (attaches routes to Host HTTP API)
|
||||
- `serverless.prepopulate.yml`: Prepopulate functions (attaches routes to Host HTTP API)
|
||||
|
||||
## Deploy order (per stage)
|
||||
|
||||
1) Deploy the layer:
|
||||
|
||||
```bash
|
||||
npx serverless deploy --config serverless.layers.yml --stage dev
|
||||
```
|
||||
|
||||
2) Deploy Host (creates the HTTP API + routes for host functions):
|
||||
|
||||
```bash
|
||||
npx serverless deploy --config serverless.host.yml --stage dev
|
||||
```
|
||||
|
||||
3) Deploy remaining services (they reuse Host's HTTP API id):
|
||||
|
||||
```bash
|
||||
npx serverless deploy --config serverless.admin.yml --stage dev
|
||||
npx serverless deploy --config serverless.user.yml --stage dev
|
||||
npx serverless deploy --config serverless.prepopulate.yml --stage dev
|
||||
```
|
||||
|
||||
## Deploy a single function
|
||||
|
||||
```bash
|
||||
npx serverless deploy function --config serverless.host.yml --stage dev -f getHosts
|
||||
```
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.2.5",
|
||||
"serverless-esbuild": "^1.55.1",
|
||||
"serverless-offline": "^14.4.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.4",
|
||||
|
||||
@@ -554,20 +554,6 @@ model Frequencies {
|
||||
@@schema("mst")
|
||||
}
|
||||
|
||||
model NavigationModes {
|
||||
id Int @id @default(autoincrement())
|
||||
navigationModeName String @unique @map("navigation_mode_name") @db.VarChar(30)
|
||||
navigationModeIcon String @map("navigation_mode_icon") @db.VarChar(500)
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
ActivityNavigationModes ActivityNavigationModes[]
|
||||
|
||||
@@map("navigation_modes")
|
||||
@@schema("mst")
|
||||
}
|
||||
|
||||
model TransportModes {
|
||||
id Int @id @default(autoincrement())
|
||||
transportModeName String @unique @map("transport_mode_name") @db.VarChar(60)
|
||||
@@ -1456,8 +1442,7 @@ model ActivityNavigationModes {
|
||||
id Int @id @default(autoincrement())
|
||||
activityXid Int @map("activity_xid")
|
||||
activity Activities @relation(fields: [activityXid], references: [id], onDelete: Cascade)
|
||||
navigationModeXid Int @map("navigation_mode_xid")
|
||||
navigationMode NavigationModes @relation(fields: [navigationModeXid], references: [id], onDelete: Restrict)
|
||||
navigationModeName String @map("navigation_mode_name") @db.VarChar(30)
|
||||
isInActivityChargeable Boolean @default(false) @map("is_in_activity_chargeable")
|
||||
navigationModesBasePrice Int @map("navigation_modes_base_price")
|
||||
navigationModesTotalPrice Int @map("navigation_modes_total_price")
|
||||
@@ -1637,8 +1622,8 @@ model Cancellations {
|
||||
scheduleHeaderXid Int @map("schedule_header_xid")
|
||||
scheduleHeader ScheduleHeader @relation(fields: [scheduleHeaderXid], references: [id], onDelete: Cascade)
|
||||
occurenceDate DateTime? @map("occurence_date")
|
||||
startTime String? @map("start_time") @db.VarChar(30)
|
||||
endTime String? @map("end_time") @db.VarChar(30)
|
||||
startTime String? @map("start_time") @db.VarChar(30)
|
||||
endTime String? @map("end_time") @db.VarChar(30)
|
||||
cancellationReason String? @map("cancellation_reason")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@ -268,9 +268,9 @@ async function main() {
|
||||
create: { interestName: 'Nightlife & Events', displayOrder: 10, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/NightlifeandEvents.png', interestCode: 'NE' },
|
||||
});
|
||||
const furfam = await prisma.interests.upsert({
|
||||
where: { interestName: 'Fur Fam' },
|
||||
where: { interestName: 'Pet space' },
|
||||
update: {},
|
||||
create: { interestName: 'Fur Fam', displayOrder: 11, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/petspace.jpg', interestCode: 'PS' },
|
||||
create: { interestName: 'Pet space', displayOrder: 11, interestColor: 'Blue', interestImage: 'https://minglar-dev-bucket.s3.ap-south-1.amazonaws.com/StaticImages/InterestTypes/petspace.jpg', interestCode: 'PS' },
|
||||
});
|
||||
const dogoodfeelgood = await prisma.interests.upsert({
|
||||
where: { interestName: 'Do Good, Feel Good' },
|
||||
@@ -693,16 +693,6 @@ async function main() {
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
// ✅ Navigation Modes
|
||||
await prisma.navigationModes.createMany({
|
||||
data: [
|
||||
{ navigationModeName: 'Elephant Ride', navigationModeIcon: '🚗' },
|
||||
{ navigationModeName: 'Horse Ride', navigationModeIcon: '🏍️' },
|
||||
{ navigationModeName: 'Camel Ride', navigationModeIcon: '🚶' },
|
||||
],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
// ✅ Transport Modes
|
||||
await prisma.transportModes.createMany({
|
||||
data: [
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
service: minglar-admin
|
||||
|
||||
useDotenv: true
|
||||
|
||||
params:
|
||||
dev:
|
||||
stage: dev
|
||||
test:
|
||||
stage: test
|
||||
uat:
|
||||
stage: uat
|
||||
|
||||
provider:
|
||||
name: aws
|
||||
runtime: nodejs22.x
|
||||
region: ap-south-1
|
||||
stage: ${opt:stage, 'dev'}
|
||||
versionFunctions: false
|
||||
memorySize: 512
|
||||
layers:
|
||||
- ${cf:minglar-layers-${sls:stage}.PrismaLambdaLayerQualifiedArn}
|
||||
httpApi:
|
||||
id: ${cf:minglar-host-${sls:stage}.HttpApiId}
|
||||
apiGateway:
|
||||
binaryMediaTypes:
|
||||
- '*/*'
|
||||
minimumCompressionSize: 1024
|
||||
|
||||
environment:
|
||||
DATABASE_URL: ${env:DATABASE_URL}
|
||||
DB_USERNAME: ${env:DB_USERNAME}
|
||||
DB_PASSWORD: ${env:DB_PASSWORD}
|
||||
DB_DATABASE_NAME: ${env:DB_DATABASE_NAME}
|
||||
DB_HOSTNAME: ${env:DB_HOSTNAME}
|
||||
DB_PORT: ${env:DB_PORT}
|
||||
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
|
||||
BYPASS_OTP: ${env:BYPASS_OTP}
|
||||
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
|
||||
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
|
||||
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
|
||||
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
|
||||
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
|
||||
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
|
||||
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
|
||||
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
|
||||
JWT_SECRET: ${env:JWT_SECRET}
|
||||
JWT_ACCESS_EXPIRATION_MINUTES: ${env:JWT_ACCESS_EXPIRATION_MINUTES}
|
||||
JWT_REFRESH_EXPIRATION_DAYS: ${env:JWT_REFRESH_EXPIRATION_DAYS}
|
||||
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: ${env:JWT_RESET_PASSWORD_EXPIRATION_MINUTES}
|
||||
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: ${env:JWT_VERIFY_EMAIL_EXPIRATION_MINUTES}
|
||||
SALT_ROUNDS: ${env:SALT_ROUNDS}
|
||||
NODE_ENV: ${env:NODE_ENV}
|
||||
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
|
||||
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
|
||||
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
|
||||
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
|
||||
HOST_LINK: ${env:HOST_LINK}
|
||||
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
|
||||
|
||||
iam:
|
||||
role:
|
||||
statements:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- s3:PutObject
|
||||
- s3:GetObject
|
||||
- s3:DeleteObject
|
||||
- s3:ListBucket
|
||||
Resource:
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
|
||||
|
||||
custom:
|
||||
serverless-offline:
|
||||
reloadHandler: true
|
||||
|
||||
build:
|
||||
esbuild:
|
||||
bundle: true
|
||||
minify: true
|
||||
sourcemap: false
|
||||
target: node22
|
||||
platform: node
|
||||
external:
|
||||
- '@prisma/client'
|
||||
- '.prisma/client'
|
||||
- '.prisma'
|
||||
- '@prisma/adapter-pg'
|
||||
- 'pg'
|
||||
- 'zod'
|
||||
- '@aws-sdk/*'
|
||||
- '@smithy/*'
|
||||
- '@aws-crypto/*'
|
||||
exclude:
|
||||
- 'aws-sdk'
|
||||
- '@aws-sdk/*'
|
||||
- '@smithy/*'
|
||||
- '@aws-crypto/*'
|
||||
- '@prisma/adapter-pg'
|
||||
- '@prisma/client'
|
||||
- '.prisma'
|
||||
- '.prisma/client'
|
||||
- 'pg'
|
||||
- 'zod'
|
||||
- 'pg-*'
|
||||
- 'postgres-*'
|
||||
- 'pgpass'
|
||||
- 'split2'
|
||||
- 'xtend'
|
||||
|
||||
package:
|
||||
individually: true
|
||||
excludeDevDependencies: true
|
||||
patterns:
|
||||
- '!node_modules/**'
|
||||
- '!node_modules/@prisma/**'
|
||||
- '!node_modules/.prisma/**'
|
||||
- '!**/*.test.js'
|
||||
- '!**/*.spec.js'
|
||||
- '!**/test/**'
|
||||
- '!**/__tests__/**'
|
||||
- '!package-lock.json'
|
||||
- '!yarn.lock'
|
||||
- '!README.md'
|
||||
- '!*.config.js'
|
||||
- '!.git/**'
|
||||
- '!.github/**'
|
||||
|
||||
functions:
|
||||
- ${file(./serverless/functions/minglaradmin.yml)}
|
||||
|
||||
plugins:
|
||||
- serverless-offline
|
||||
@@ -1,132 +0,0 @@
|
||||
service: minglar-host
|
||||
|
||||
useDotenv: true
|
||||
|
||||
params:
|
||||
dev:
|
||||
stage: dev
|
||||
test:
|
||||
stage: test
|
||||
uat:
|
||||
stage: uat
|
||||
|
||||
provider:
|
||||
name: aws
|
||||
runtime: nodejs22.x
|
||||
region: ap-south-1
|
||||
stage: ${opt:stage, 'dev'}
|
||||
versionFunctions: false
|
||||
memorySize: 512
|
||||
layers:
|
||||
- ${cf:minglar-layers-${sls:stage}.PrismaLambdaLayerQualifiedArn}
|
||||
apiGateway:
|
||||
binaryMediaTypes:
|
||||
- '*/*'
|
||||
minimumCompressionSize: 1024
|
||||
|
||||
environment:
|
||||
DATABASE_URL: ${env:DATABASE_URL}
|
||||
DB_USERNAME: ${env:DB_USERNAME}
|
||||
DB_PASSWORD: ${env:DB_PASSWORD}
|
||||
DB_DATABASE_NAME: ${env:DB_DATABASE_NAME}
|
||||
DB_HOSTNAME: ${env:DB_HOSTNAME}
|
||||
DB_PORT: ${env:DB_PORT}
|
||||
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
|
||||
BYPASS_OTP: ${env:BYPASS_OTP}
|
||||
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
|
||||
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
|
||||
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
|
||||
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
|
||||
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
|
||||
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
|
||||
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
|
||||
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
|
||||
JWT_SECRET: ${env:JWT_SECRET}
|
||||
JWT_ACCESS_EXPIRATION_MINUTES: ${env:JWT_ACCESS_EXPIRATION_MINUTES}
|
||||
JWT_REFRESH_EXPIRATION_DAYS: ${env:JWT_REFRESH_EXPIRATION_DAYS}
|
||||
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: ${env:JWT_RESET_PASSWORD_EXPIRATION_MINUTES}
|
||||
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: ${env:JWT_VERIFY_EMAIL_EXPIRATION_MINUTES}
|
||||
SALT_ROUNDS: ${env:SALT_ROUNDS}
|
||||
NODE_ENV: ${env:NODE_ENV}
|
||||
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
|
||||
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
|
||||
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
|
||||
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
|
||||
HOST_LINK: ${env:HOST_LINK}
|
||||
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
|
||||
|
||||
iam:
|
||||
role:
|
||||
statements:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- s3:PutObject
|
||||
- s3:GetObject
|
||||
- s3:DeleteObject
|
||||
- s3:ListBucket
|
||||
Resource:
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
|
||||
|
||||
custom:
|
||||
serverless-offline:
|
||||
reloadHandler: true
|
||||
|
||||
build:
|
||||
esbuild:
|
||||
bundle: true
|
||||
minify: true
|
||||
sourcemap: false
|
||||
target: node22
|
||||
platform: node
|
||||
external:
|
||||
- '@prisma/client'
|
||||
- '.prisma/client'
|
||||
- '.prisma'
|
||||
- '@prisma/adapter-pg'
|
||||
- 'pg'
|
||||
- 'zod'
|
||||
- '@aws-sdk/*'
|
||||
- '@smithy/*'
|
||||
- '@aws-crypto/*'
|
||||
exclude:
|
||||
- 'aws-sdk'
|
||||
- '@aws-sdk/*'
|
||||
- '@smithy/*'
|
||||
- '@aws-crypto/*'
|
||||
- '@prisma/adapter-pg'
|
||||
- '@prisma/client'
|
||||
- '.prisma'
|
||||
- '.prisma/client'
|
||||
- 'pg'
|
||||
- 'zod'
|
||||
- 'pg-*'
|
||||
- 'postgres-*'
|
||||
- 'pgpass'
|
||||
- 'split2'
|
||||
- 'xtend'
|
||||
|
||||
package:
|
||||
individually: true
|
||||
excludeDevDependencies: true
|
||||
patterns:
|
||||
- '!node_modules/**'
|
||||
- '!node_modules/@prisma/**'
|
||||
- '!node_modules/.prisma/**'
|
||||
- '!**/*.test.js'
|
||||
- '!**/*.spec.js'
|
||||
- '!**/test/**'
|
||||
- '!**/__tests__/**'
|
||||
- '!package-lock.json'
|
||||
- '!yarn.lock'
|
||||
- '!README.md'
|
||||
- '!*.config.js'
|
||||
- '!.git/**'
|
||||
- '!.github/**'
|
||||
|
||||
functions:
|
||||
- ${file(./serverless/functions/host.yml)}
|
||||
- ${file(./serverless/functions/pqq.yml)}
|
||||
|
||||
plugins:
|
||||
- serverless-offline
|
||||
@@ -1,30 +0,0 @@
|
||||
service: minglar-layers
|
||||
|
||||
useDotenv: true
|
||||
|
||||
params:
|
||||
dev:
|
||||
stage: dev
|
||||
test:
|
||||
stage: test
|
||||
uat:
|
||||
stage: uat
|
||||
|
||||
provider:
|
||||
name: aws
|
||||
runtime: nodejs22.x
|
||||
region: ap-south-1
|
||||
stage: ${opt:stage, 'dev'}
|
||||
versionFunctions: false
|
||||
|
||||
# Define layers (deployed once; other stacks reference via cf output)
|
||||
layers:
|
||||
prisma:
|
||||
path: layers/prisma
|
||||
name: ${self:service}-prisma-layer-${sls:stage}
|
||||
description: Prisma 7 client with pg driver adapter (no binary engines)
|
||||
compatibleRuntimes:
|
||||
- nodejs22.x
|
||||
retain: false
|
||||
|
||||
plugins: []
|
||||
@@ -1,133 +0,0 @@
|
||||
service: minglar-prepopulate
|
||||
|
||||
useDotenv: true
|
||||
|
||||
params:
|
||||
dev:
|
||||
stage: dev
|
||||
test:
|
||||
stage: test
|
||||
uat:
|
||||
stage: uat
|
||||
|
||||
provider:
|
||||
name: aws
|
||||
runtime: nodejs22.x
|
||||
region: ap-south-1
|
||||
stage: ${opt:stage, 'dev'}
|
||||
versionFunctions: false
|
||||
memorySize: 512
|
||||
layers:
|
||||
- ${cf:minglar-layers-${sls:stage}.PrismaLambdaLayerQualifiedArn}
|
||||
httpApi:
|
||||
id: ${cf:minglar-host-${sls:stage}.HttpApiId}
|
||||
apiGateway:
|
||||
binaryMediaTypes:
|
||||
- '*/*'
|
||||
minimumCompressionSize: 1024
|
||||
|
||||
environment:
|
||||
DATABASE_URL: ${env:DATABASE_URL}
|
||||
DB_USERNAME: ${env:DB_USERNAME}
|
||||
DB_PASSWORD: ${env:DB_PASSWORD}
|
||||
DB_DATABASE_NAME: ${env:DB_DATABASE_NAME}
|
||||
DB_HOSTNAME: ${env:DB_HOSTNAME}
|
||||
DB_PORT: ${env:DB_PORT}
|
||||
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
|
||||
BYPASS_OTP: ${env:BYPASS_OTP}
|
||||
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
|
||||
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
|
||||
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
|
||||
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
|
||||
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
|
||||
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
|
||||
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
|
||||
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
|
||||
JWT_SECRET: ${env:JWT_SECRET}
|
||||
JWT_ACCESS_EXPIRATION_MINUTES: ${env:JWT_ACCESS_EXPIRATION_MINUTES}
|
||||
JWT_REFRESH_EXPIRATION_DAYS: ${env:JWT_REFRESH_EXPIRATION_DAYS}
|
||||
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: ${env:JWT_RESET_PASSWORD_EXPIRATION_MINUTES}
|
||||
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: ${env:JWT_VERIFY_EMAIL_EXPIRATION_MINUTES}
|
||||
SALT_ROUNDS: ${env:SALT_ROUNDS}
|
||||
NODE_ENV: ${env:NODE_ENV}
|
||||
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
|
||||
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
|
||||
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
|
||||
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
|
||||
HOST_LINK: ${env:HOST_LINK}
|
||||
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
|
||||
|
||||
iam:
|
||||
role:
|
||||
statements:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- s3:PutObject
|
||||
- s3:GetObject
|
||||
- s3:DeleteObject
|
||||
- s3:ListBucket
|
||||
Resource:
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
|
||||
|
||||
custom:
|
||||
serverless-offline:
|
||||
reloadHandler: true
|
||||
|
||||
build:
|
||||
esbuild:
|
||||
bundle: true
|
||||
minify: true
|
||||
sourcemap: false
|
||||
target: node22
|
||||
platform: node
|
||||
external:
|
||||
- '@prisma/client'
|
||||
- '.prisma/client'
|
||||
- '.prisma'
|
||||
- '@prisma/adapter-pg'
|
||||
- 'pg'
|
||||
- 'zod'
|
||||
- '@aws-sdk/*'
|
||||
- '@smithy/*'
|
||||
- '@aws-crypto/*'
|
||||
exclude:
|
||||
- 'aws-sdk'
|
||||
- '@aws-sdk/*'
|
||||
- '@smithy/*'
|
||||
- '@aws-crypto/*'
|
||||
- '@prisma/adapter-pg'
|
||||
- '@prisma/client'
|
||||
- '.prisma'
|
||||
- '.prisma/client'
|
||||
- 'pg'
|
||||
- 'zod'
|
||||
- 'pg-*'
|
||||
- 'postgres-*'
|
||||
- 'pgpass'
|
||||
- 'split2'
|
||||
- 'xtend'
|
||||
|
||||
package:
|
||||
individually: true
|
||||
excludeDevDependencies: true
|
||||
patterns:
|
||||
- '!node_modules/**'
|
||||
- '!node_modules/@prisma/**'
|
||||
- '!node_modules/.prisma/**'
|
||||
- '!**/*.test.js'
|
||||
- '!**/*.spec.js'
|
||||
- '!**/test/**'
|
||||
- '!**/__tests__/**'
|
||||
- '!package-lock.json'
|
||||
- '!yarn.lock'
|
||||
- '!README.md'
|
||||
- '!*.config.js'
|
||||
- '!.git/**'
|
||||
- '!.github/**'
|
||||
|
||||
functions:
|
||||
- ${file(./serverless/functions/prepopulate.yml)}
|
||||
|
||||
plugins:
|
||||
- serverless-offline
|
||||
@@ -1,133 +0,0 @@
|
||||
service: minglar-user
|
||||
|
||||
useDotenv: true
|
||||
|
||||
params:
|
||||
dev:
|
||||
stage: dev
|
||||
test:
|
||||
stage: test
|
||||
uat:
|
||||
stage: uat
|
||||
|
||||
provider:
|
||||
name: aws
|
||||
runtime: nodejs22.x
|
||||
region: ap-south-1
|
||||
stage: ${opt:stage, 'dev'}
|
||||
versionFunctions: false
|
||||
memorySize: 512
|
||||
layers:
|
||||
- ${cf:minglar-layers-${sls:stage}.PrismaLambdaLayerQualifiedArn}
|
||||
httpApi:
|
||||
id: ${cf:minglar-host-${sls:stage}.HttpApiId}
|
||||
apiGateway:
|
||||
binaryMediaTypes:
|
||||
- '*/*'
|
||||
minimumCompressionSize: 1024
|
||||
|
||||
environment:
|
||||
DATABASE_URL: ${env:DATABASE_URL}
|
||||
DB_USERNAME: ${env:DB_USERNAME}
|
||||
DB_PASSWORD: ${env:DB_PASSWORD}
|
||||
DB_DATABASE_NAME: ${env:DB_DATABASE_NAME}
|
||||
DB_HOSTNAME: ${env:DB_HOSTNAME}
|
||||
DB_PORT: ${env:DB_PORT}
|
||||
BY_PASS_EMAIL: ${env:BY_PASS_EMAIL}
|
||||
BYPASS_OTP: ${env:BYPASS_OTP}
|
||||
BREVO_EMAIL_API_KEY: ${env:BREVO_EMAIL_API_KEY}
|
||||
BREVO_API_BASEURL: ${env:BREVO_API_BASEURL}
|
||||
BREVO_FROM_EMAIL: ${env:BREVO_FROM_EMAIL}
|
||||
BREVO_SMTP_HOST: ${env:BREVO_SMTP_HOST}
|
||||
BREVO_SMTP_PORT: ${env:BREVO_SMTP_PORT}
|
||||
BREVO_SMTP_USER: ${env:BREVO_SMTP_USER}
|
||||
BREVO_SMTP_PASS: ${env:BREVO_SMTP_PASS}
|
||||
REFRESH_TOKEN_SECRET: ${env:REFRESH_TOKEN_SECRET}
|
||||
JWT_SECRET: ${env:JWT_SECRET}
|
||||
JWT_ACCESS_EXPIRATION_MINUTES: ${env:JWT_ACCESS_EXPIRATION_MINUTES}
|
||||
JWT_REFRESH_EXPIRATION_DAYS: ${env:JWT_REFRESH_EXPIRATION_DAYS}
|
||||
JWT_RESET_PASSWORD_EXPIRATION_MINUTES: ${env:JWT_RESET_PASSWORD_EXPIRATION_MINUTES}
|
||||
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: ${env:JWT_VERIFY_EMAIL_EXPIRATION_MINUTES}
|
||||
SALT_ROUNDS: ${env:SALT_ROUNDS}
|
||||
NODE_ENV: ${env:NODE_ENV}
|
||||
S3_BUCKET_NAME: ${env:S3_BUCKET_NAME}
|
||||
MINGLAR_ADMIN_NAME: ${env:MINGLAR_ADMIN_NAME}
|
||||
MINGLAR_ADMIN_EMAIL: ${env:MINGLAR_ADMIN_EMAIL}
|
||||
AM_INVITATION_LINK: ${env:AM_INVITATION_LINK}
|
||||
HOST_LINK: ${env:HOST_LINK}
|
||||
HOST_LINK_PQ: ${env:HOST_LINK_PQ}
|
||||
|
||||
iam:
|
||||
role:
|
||||
statements:
|
||||
- Effect: Allow
|
||||
Action:
|
||||
- s3:PutObject
|
||||
- s3:GetObject
|
||||
- s3:DeleteObject
|
||||
- s3:ListBucket
|
||||
Resource:
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}'
|
||||
- 'arn:aws:s3:::${env:S3_BUCKET_NAME}/*'
|
||||
|
||||
custom:
|
||||
serverless-offline:
|
||||
reloadHandler: true
|
||||
|
||||
build:
|
||||
esbuild:
|
||||
bundle: true
|
||||
minify: true
|
||||
sourcemap: false
|
||||
target: node22
|
||||
platform: node
|
||||
external:
|
||||
- '@prisma/client'
|
||||
- '.prisma/client'
|
||||
- '.prisma'
|
||||
- '@prisma/adapter-pg'
|
||||
- 'pg'
|
||||
- 'zod'
|
||||
- '@aws-sdk/*'
|
||||
- '@smithy/*'
|
||||
- '@aws-crypto/*'
|
||||
exclude:
|
||||
- 'aws-sdk'
|
||||
- '@aws-sdk/*'
|
||||
- '@smithy/*'
|
||||
- '@aws-crypto/*'
|
||||
- '@prisma/adapter-pg'
|
||||
- '@prisma/client'
|
||||
- '.prisma'
|
||||
- '.prisma/client'
|
||||
- 'pg'
|
||||
- 'zod'
|
||||
- 'pg-*'
|
||||
- 'postgres-*'
|
||||
- 'pgpass'
|
||||
- 'split2'
|
||||
- 'xtend'
|
||||
|
||||
package:
|
||||
individually: true
|
||||
excludeDevDependencies: true
|
||||
patterns:
|
||||
- '!node_modules/**'
|
||||
- '!node_modules/@prisma/**'
|
||||
- '!node_modules/.prisma/**'
|
||||
- '!**/*.test.js'
|
||||
- '!**/*.spec.js'
|
||||
- '!**/test/**'
|
||||
- '!**/__tests__/**'
|
||||
- '!package-lock.json'
|
||||
- '!yarn.lock'
|
||||
- '!README.md'
|
||||
- '!*.config.js'
|
||||
- '!.git/**'
|
||||
- '!.github/**'
|
||||
|
||||
functions:
|
||||
- ${file(./serverless/functions/user.yml)}
|
||||
|
||||
plugins:
|
||||
- serverless-offline
|
||||
@@ -1,4 +1,4 @@
|
||||
service: minglar
|
||||
service: minglarDev
|
||||
|
||||
|
||||
useDotenv: true
|
||||
@@ -23,7 +23,7 @@ provider:
|
||||
layers:
|
||||
# Use the exported stack output so deploy function works (expects a string ARN)
|
||||
# For offline/local, fall back to an empty string so the CF lookup is optional.
|
||||
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn}
|
||||
- ${cf:${self:service}-${sls:stage}.PrismaLambdaLayerQualifiedArn, ''}
|
||||
apiGateway:
|
||||
binaryMediaTypes:
|
||||
- '*/*'
|
||||
@@ -76,13 +76,13 @@ provider:
|
||||
custom:
|
||||
serverless-offline:
|
||||
reloadHandler: true
|
||||
|
||||
|
||||
build:
|
||||
esbuild:
|
||||
bundle: true
|
||||
minify: true
|
||||
sourcemap: false
|
||||
target: node22
|
||||
target: node20
|
||||
platform: node
|
||||
# Mark as external so they're not bundled into the JS
|
||||
external:
|
||||
@@ -125,7 +125,6 @@ layers:
|
||||
|
||||
package:
|
||||
individually: true
|
||||
excludeDevDependencies: true
|
||||
patterns:
|
||||
- '!node_modules/**'
|
||||
- '!node_modules/@prisma/**'
|
||||
@@ -148,6 +147,6 @@ functions:
|
||||
- ${file(./serverless/functions/prepopulate.yml)}
|
||||
- ${file(./serverless/functions/pqq.yml)}
|
||||
- ${file(./serverless/functions/user.yml)}
|
||||
|
||||
|
||||
plugins:
|
||||
- serverless-offline
|
||||
@@ -258,6 +258,22 @@ acceptAggrement:
|
||||
path: /host/Host_Admin/onboarding/accept-agreement
|
||||
method: patch
|
||||
|
||||
getLatestAgreement:
|
||||
handler: src/modules/host/handlers/Host_Admin/onboarding/getLatestAgreement.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/Host_Admin/onboarding/getLatestAgreement.*'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /host/Host_Admin/onboarding/get-latest-agreement
|
||||
method: get
|
||||
|
||||
getStepperInfo:
|
||||
handler: src/modules/host/handlers/getStepper.handler
|
||||
memorySize: 384
|
||||
@@ -276,6 +292,22 @@ getStepperInfo:
|
||||
path: /stepper
|
||||
method: get
|
||||
|
||||
updateHostProfile:
|
||||
handler: src/modules/host/handlers/updateHostProfile.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/host/handlers/updateHostProfile.*'
|
||||
- 'src/modules/host/services/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /host/profile
|
||||
method: patch
|
||||
|
||||
# Functions with S3/AWS SDK dependencies
|
||||
submitCompanyDetails:
|
||||
handler: src/modules/host/handlers/Host_Admin/onboarding/submitCompanyDetails.handler
|
||||
|
||||
@@ -346,4 +346,19 @@ getNearbyActivities:
|
||||
events:
|
||||
- httpApi:
|
||||
path: /user/activities/get-nearby-activities
|
||||
method: get
|
||||
method: get
|
||||
|
||||
addActivityToBucketInterested:
|
||||
handler: src/modules/user/handlers/activities/addToBucketInterested.handler
|
||||
memorySize: 384
|
||||
package:
|
||||
patterns:
|
||||
- 'src/modules/user/handlers/activities/**'
|
||||
- ${file(./serverless/patterns/base.yml):pattern1}
|
||||
- ${file(./serverless/patterns/base.yml):pattern2}
|
||||
- ${file(./serverless/patterns/base.yml):pattern3}
|
||||
- ${file(./serverless/patterns/base.yml):pattern4}
|
||||
events:
|
||||
- httpApi:
|
||||
path: /user/activities/add-to-bucket-interested
|
||||
method: post
|
||||
@@ -54,7 +54,7 @@ export const EquipmentDto = z.object({
|
||||
|
||||
/* ================= NAVIGATION MODE ================= */
|
||||
export const NavigationModeDto = z.object({
|
||||
navigationModeXid: z.number().int(),
|
||||
navigationModeName: z.string().optional(),
|
||||
isChargeable: z.boolean().optional(),
|
||||
totalPrice: z.number().int().optional().default(0),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../../common/utils/helper/ApiError';
|
||||
import { HostService } from '../../../services/host.service';
|
||||
@@ -25,9 +25,8 @@ export const handler = safeHandler(async (
|
||||
// Verify token and get user info
|
||||
const userInfo = await verifyHostToken(token);
|
||||
|
||||
|
||||
// Add suggestion using service
|
||||
await hostService.acceptMinglarAgreement(userInfo.id);
|
||||
// Accept agreement and get dynamic fields and PDF URL
|
||||
const result = await hostService.acceptMinglarAgreement(userInfo.id);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
@@ -38,7 +37,10 @@ export const handler = safeHandler(async (
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Application accepted successfully',
|
||||
data: null,
|
||||
data: {
|
||||
filePath: result.filePath,
|
||||
dynamicFields: result.dynamicFields,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { verifyMinglarAdminHostToken } from '../../../../../common/middlewares/jwt/authForMinglarAdminHost';
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../../common/database/prisma.lambda.service';
|
||||
import { safeHandler } from '../../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../../common/utils/helper/ApiError';
|
||||
import { HostService } from '../../../services/host.service';
|
||||
|
||||
const hostService = new HostService(prismaClient);
|
||||
|
||||
/**
|
||||
* Get latest active agreement for a specific host by hostXid.
|
||||
* Accessible for Minglar Admin / Host Admin using admin-host token.
|
||||
*/
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
||||
if (!token) {
|
||||
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
|
||||
}
|
||||
|
||||
// Validate admin/host admin token
|
||||
await verifyMinglarAdminHostToken(token);
|
||||
|
||||
const hostXidParam =
|
||||
event.queryStringParameters?.hostXid ?? event.queryStringParameters?.host_xid;
|
||||
|
||||
const hostXid = Number(hostXidParam);
|
||||
|
||||
if (!hostXidParam) {
|
||||
throw new ApiError(400, 'hostXid is required');
|
||||
}
|
||||
|
||||
if (Number.isNaN(hostXid)) {
|
||||
throw new ApiError(400, 'Invalid hostXid format');
|
||||
}
|
||||
|
||||
const agreement = await hostService.getLatestHostAgreement(hostXid);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Latest host agreement retrieved successfully',
|
||||
data: agreement,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ export const handler = safeHandler(async (
|
||||
data: {
|
||||
stepper: host?.host?.stepper || null,
|
||||
emailAddress: host.user?.emailAddress || null,
|
||||
hostId: host.user?.userRefNumber || null,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
244
src/modules/host/handlers/updateHostProfile.ts
Normal file
244
src/modules/host/handlers/updateHostProfile.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import dayjs from 'dayjs';
|
||||
import { z } from 'zod';
|
||||
import { prismaClient } from '../../../common/database/prisma.lambda.service';
|
||||
import { verifyHostToken } from '../../../common/middlewares/jwt/authForHost';
|
||||
import { safeHandler } from '../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../common/utils/helper/ApiError';
|
||||
import { ROLE } from '../../../common/utils/constants/common.constant';
|
||||
|
||||
const updateHostProfileSchema = z
|
||||
.strictObject({
|
||||
// Personal
|
||||
fullName: z.string().min(1).optional(),
|
||||
firstName: z.string().min(1).optional(),
|
||||
lastName: z.string().min(1).optional(),
|
||||
isdCode: z.string().min(1).max(6).optional(),
|
||||
mobileNumber: z.string().min(5).max(15).optional(),
|
||||
dateOfBirth: z.string().min(1).optional(),
|
||||
|
||||
// Address
|
||||
address1: z.string().min(1).optional(),
|
||||
address2: z.string().min(1).optional(),
|
||||
countryXid: z.number().int().positive().optional(),
|
||||
stateXid: z.number().int().positive().optional(),
|
||||
cityXid: z.number().int().positive().optional(),
|
||||
pinCode: z.string().min(1).optional(),
|
||||
|
||||
// explicitly forbidden
|
||||
emailAddress: z.any().optional(),
|
||||
})
|
||||
.strip();
|
||||
|
||||
function parseDob(dateOfBirth: string): Date {
|
||||
const parsed = dayjs(dateOfBirth, ['YYYY-MM-DD', 'MM/DD/YYYY', 'DD/MM/YYYY'], true);
|
||||
if (!parsed.isValid()) {
|
||||
throw new ApiError(400, 'Invalid dateOfBirth. Use YYYY-MM-DD (recommended) or MM/DD/YYYY.');
|
||||
}
|
||||
return parsed.toDate();
|
||||
}
|
||||
|
||||
function splitFullName(fullName: string): { firstName: string; lastName: string | null } {
|
||||
const parts = fullName.trim().split(/\s+/).filter(Boolean);
|
||||
const firstName = parts[0] || '';
|
||||
const lastName = parts.length > 1 ? parts.slice(1).join(' ') : null;
|
||||
return { firstName, lastName };
|
||||
}
|
||||
|
||||
function getAuthToken(event: APIGatewayProxyEvent): string {
|
||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token'];
|
||||
if (!token) {
|
||||
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function parseJsonBody(event: APIGatewayProxyEvent): any {
|
||||
try {
|
||||
return event.body ? JSON.parse(event.body) : {};
|
||||
} catch {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
}
|
||||
|
||||
function validateBody(body: any) {
|
||||
const parsed = updateHostProfileSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
throw new ApiError(400, parsed.error.issues.map((i) => i.message).join(', '));
|
||||
}
|
||||
if (parsed.data.emailAddress !== undefined) {
|
||||
throw new ApiError(400, 'Email address cannot be updated.');
|
||||
}
|
||||
return parsed.data;
|
||||
}
|
||||
|
||||
function normalizeNameFields(data: any): { firstName?: string; lastName?: string | null } {
|
||||
if (data.fullName && !data.firstName && !data.lastName) {
|
||||
const split = splitFullName(data.fullName);
|
||||
return { firstName: split.firstName, lastName: split.lastName };
|
||||
}
|
||||
return { firstName: data.firstName, lastName: data.lastName };
|
||||
}
|
||||
|
||||
function buildAddressInput(data: any) {
|
||||
return {
|
||||
address1: data.address1,
|
||||
address2: data.address2,
|
||||
countryXid: data.countryXid,
|
||||
stateXid: data.stateXid,
|
||||
cityXid: data.cityXid,
|
||||
pinCode: data.pinCode,
|
||||
};
|
||||
}
|
||||
|
||||
function hasAnyDefined(obj: Record<string, unknown>) {
|
||||
return Object.values(obj).some((v) => v !== undefined);
|
||||
}
|
||||
|
||||
async function ensureHostUser(tx: any, userId: number) {
|
||||
const user = await tx.user.findUnique({
|
||||
where: { id: userId, isActive: true },
|
||||
select: { id: true, roleXid: true },
|
||||
});
|
||||
|
||||
if (!user) throw new ApiError(404, 'User not found');
|
||||
if (user.roleXid !== ROLE.HOST) throw new ApiError(403, 'Access denied.');
|
||||
}
|
||||
|
||||
async function updateUserIfNeeded(tx: any, userId: number, input: { firstName?: string; lastName?: string | null; isdCode?: string; mobileNumber?: string; dateOfBirth?: string }) {
|
||||
const userUpdateData: any = {};
|
||||
if (input.firstName !== undefined) userUpdateData.firstName = input.firstName || null;
|
||||
if (input.lastName !== undefined) userUpdateData.lastName = input.lastName;
|
||||
if (input.isdCode !== undefined) userUpdateData.isdCode = input.isdCode || null;
|
||||
if (input.mobileNumber !== undefined) userUpdateData.mobileNumber = input.mobileNumber || null;
|
||||
if (input.dateOfBirth !== undefined) {
|
||||
userUpdateData.dateOfBirth = input.dateOfBirth ? parseDob(input.dateOfBirth) : null;
|
||||
}
|
||||
|
||||
if (!hasAnyDefined(userUpdateData)) return;
|
||||
|
||||
await tx.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
...userUpdateData,
|
||||
isProfileUpdated: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function upsertAddressIfNeeded(tx: any, userId: number, addressData: Record<string, any>) {
|
||||
if (!hasAnyDefined(addressData)) return;
|
||||
|
||||
const existingAddress = await tx.userAddressDetails.findFirst({
|
||||
where: { userXid: userId, isActive: true },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const addressUpdateData: any = {};
|
||||
if (addressData.address1 !== undefined) addressUpdateData.address1 = addressData.address1;
|
||||
if (addressData.address2 !== undefined) addressUpdateData.address2 = addressData.address2;
|
||||
if (addressData.countryXid !== undefined) addressUpdateData.countryXid = addressData.countryXid;
|
||||
if (addressData.stateXid !== undefined) addressUpdateData.stateXid = addressData.stateXid;
|
||||
if (addressData.cityXid !== undefined) addressUpdateData.cityXid = addressData.cityXid;
|
||||
if (addressData.pinCode !== undefined) addressUpdateData.pinCode = addressData.pinCode;
|
||||
|
||||
if (existingAddress) {
|
||||
await tx.userAddressDetails.update({
|
||||
where: { id: existingAddress.id },
|
||||
data: addressUpdateData,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const required = ['address1', 'countryXid', 'stateXid', 'cityXid', 'pinCode'] as const;
|
||||
const missing = required.filter((k) => addressData[k] === undefined);
|
||||
if (missing.length) {
|
||||
throw new ApiError(400, `Missing required address fields: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
await tx.userAddressDetails.create({
|
||||
data: {
|
||||
userXid: userId,
|
||||
...addressUpdateData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function getProfileSnapshot(tx: any, userId: number) {
|
||||
const updated = await tx.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
emailAddress: true,
|
||||
isdCode: true,
|
||||
mobileNumber: true,
|
||||
dateOfBirth: true,
|
||||
profileImage: true,
|
||||
isProfileUpdated: true,
|
||||
userAddressDetails: {
|
||||
where: { isActive: true },
|
||||
take: 1,
|
||||
select: {
|
||||
id: true,
|
||||
address1: true,
|
||||
address2: true,
|
||||
countryXid: true,
|
||||
stateXid: true,
|
||||
cityXid: true,
|
||||
pinCode: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
user: updated,
|
||||
address: updated?.userAddressDetails?.[0] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context,
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
const token = getAuthToken(event);
|
||||
const userInfo = await verifyHostToken(token);
|
||||
const userId = Number(userInfo.id);
|
||||
if (!userId || Number.isNaN(userId)) {
|
||||
throw new ApiError(400, 'Invalid user id');
|
||||
}
|
||||
|
||||
const body = parseJsonBody(event);
|
||||
const data = validateBody(body);
|
||||
const name = normalizeNameFields(data);
|
||||
const address = buildAddressInput(data);
|
||||
|
||||
const result = await prismaClient.$transaction(async (tx) => {
|
||||
await ensureHostUser(tx, userId);
|
||||
await updateUserIfNeeded(tx, userId, {
|
||||
firstName: name.firstName,
|
||||
lastName: name.lastName,
|
||||
isdCode: data.isdCode,
|
||||
mobileNumber: data.mobileNumber,
|
||||
dateOfBirth: data.dateOfBirth,
|
||||
});
|
||||
await upsertAddressIfNeeded(tx, userId, address);
|
||||
return getProfileSnapshot(tx, userId);
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: 'Profile updated successfully',
|
||||
data: result,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -391,6 +391,22 @@ const s3 = new AWS.S3({
|
||||
region: config.aws.region,
|
||||
});
|
||||
|
||||
type UpdateHostProfileInput = {
|
||||
firstName?: string;
|
||||
lastName?: string | null;
|
||||
isdCode?: string;
|
||||
mobileNumber?: string;
|
||||
dateOfBirth?: Date;
|
||||
address?: {
|
||||
address1?: string;
|
||||
address2?: string;
|
||||
countryXid?: number;
|
||||
stateXid?: number;
|
||||
cityXid?: number;
|
||||
pinCode?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class HostService {
|
||||
constructor(private prisma: PrismaClient) { }
|
||||
@@ -415,8 +431,8 @@ export class HostService {
|
||||
});
|
||||
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: user_xid },
|
||||
select: { id: true, emailAddress: true },
|
||||
where: { id: user_xid, isActive: true },
|
||||
select: { id: true, emailAddress: true, userRefNumber: true },
|
||||
});
|
||||
return { host, user };
|
||||
}
|
||||
@@ -465,6 +481,39 @@ export class HostService {
|
||||
profileImage: true,
|
||||
userStatus: true,
|
||||
userRefNumber: true,
|
||||
userAddressDetails: {
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
address1: true,
|
||||
address2: true,
|
||||
locationAddress: true,
|
||||
locationLat: true,
|
||||
locationLong: true,
|
||||
pinCode: true,
|
||||
cityXid: true,
|
||||
cities: {
|
||||
select: {
|
||||
id: true,
|
||||
cityName: true,
|
||||
}
|
||||
},
|
||||
stateXid: true,
|
||||
states: {
|
||||
select: {
|
||||
id: true,
|
||||
stateName: true,
|
||||
}
|
||||
},
|
||||
countryXid: true,
|
||||
country: {
|
||||
select: {
|
||||
id: true,
|
||||
countryName: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
companyTypes: {
|
||||
@@ -577,6 +626,114 @@ export class HostService {
|
||||
return this.prisma.user.delete({ where: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the logged-in Host's personal profile details.
|
||||
* Email is intentionally NOT editable here.
|
||||
*/
|
||||
async updateHostProfileDetails(userId: number, input: UpdateHostProfileInput) {
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
const user = await tx.user.findUnique({
|
||||
where: { id: userId, isActive: true },
|
||||
select: { id: true, roleXid: true },
|
||||
});
|
||||
|
||||
if (!user) throw new ApiError(404, 'User not found');
|
||||
if (user.roleXid !== ROLE.HOST) throw new ApiError(403, 'Access denied.');
|
||||
|
||||
// 1) Update `User` (whitelist only)
|
||||
const userUpdateData: any = {};
|
||||
if (input.firstName !== undefined) userUpdateData.firstName = input.firstName || null;
|
||||
if (input.lastName !== undefined) userUpdateData.lastName = input.lastName;
|
||||
if (input.isdCode !== undefined) userUpdateData.isdCode = input.isdCode || null;
|
||||
if (input.mobileNumber !== undefined) userUpdateData.mobileNumber = input.mobileNumber || null;
|
||||
if (input.dateOfBirth !== undefined) userUpdateData.dateOfBirth = input.dateOfBirth;
|
||||
|
||||
if (Object.keys(userUpdateData).length > 0) {
|
||||
await tx.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
...userUpdateData,
|
||||
isProfileUpdated: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 2) Update/Create `UserAddressDetails` (if any address field sent)
|
||||
const addressData = input.address || {};
|
||||
const hasAnyAddressField = Object.values(addressData).some((v) => v !== undefined);
|
||||
|
||||
if (hasAnyAddressField) {
|
||||
const existingAddress = await tx.userAddressDetails.findFirst({
|
||||
where: { userXid: userId, isActive: true },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const addressUpdateData: any = {};
|
||||
if (addressData.address1 !== undefined) addressUpdateData.address1 = addressData.address1;
|
||||
if (addressData.address2 !== undefined) addressUpdateData.address2 = addressData.address2;
|
||||
if (addressData.countryXid !== undefined) addressUpdateData.countryXid = addressData.countryXid;
|
||||
if (addressData.stateXid !== undefined) addressUpdateData.stateXid = addressData.stateXid;
|
||||
if (addressData.cityXid !== undefined) addressUpdateData.cityXid = addressData.cityXid;
|
||||
if (addressData.pinCode !== undefined) addressUpdateData.pinCode = addressData.pinCode;
|
||||
|
||||
if (existingAddress) {
|
||||
await tx.userAddressDetails.update({
|
||||
where: { id: existingAddress.id },
|
||||
data: addressUpdateData,
|
||||
});
|
||||
} else {
|
||||
const required = ['address1', 'countryXid', 'stateXid', 'cityXid', 'pinCode'] as const;
|
||||
const missing = required.filter((k) => addressData[k] === undefined);
|
||||
|
||||
if (missing.length) {
|
||||
throw new ApiError(400, `Missing required address fields: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
await tx.userAddressDetails.create({
|
||||
data: {
|
||||
userXid: userId,
|
||||
...addressUpdateData,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Return updated profile snapshot (including read-only email)
|
||||
const updated = await tx.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
emailAddress: true,
|
||||
isdCode: true,
|
||||
mobileNumber: true,
|
||||
dateOfBirth: true,
|
||||
profileImage: true,
|
||||
isProfileUpdated: true,
|
||||
userAddressDetails: {
|
||||
where: { isActive: true },
|
||||
take: 1,
|
||||
select: {
|
||||
id: true,
|
||||
address1: true,
|
||||
address2: true,
|
||||
countryXid: true,
|
||||
stateXid: true,
|
||||
cityXid: true,
|
||||
pinCode: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
user: updated,
|
||||
address: updated?.userAddressDetails?.[0] ?? null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getHostByEmail(email: string): Promise<User> {
|
||||
return this.prisma.user.findUnique({ where: { emailAddress: email } });
|
||||
}
|
||||
@@ -919,55 +1076,150 @@ export class HostService {
|
||||
acceptDate,
|
||||
};
|
||||
|
||||
const pdfBuffer = await renderAgreementPdf(agreementVars);
|
||||
let pdfUrl: string | null = null;
|
||||
|
||||
const existingCount = await this.prisma.hostAgreement.count({
|
||||
where: { hostXid: host.id, isActive: true },
|
||||
});
|
||||
try {
|
||||
const pdfBuffer = await renderAgreementPdf(agreementVars);
|
||||
|
||||
const nextVersionNumber = `AG${existingCount + 1}`;
|
||||
const baseKey = `Documents/Host/${host.id}/agreements/${nextVersionNumber}`;
|
||||
|
||||
const pdfKey = `${baseKey}.pdf`;
|
||||
|
||||
await s3
|
||||
.upload({
|
||||
Bucket: config.aws.bucketName,
|
||||
Key: pdfKey,
|
||||
Body: pdfBuffer,
|
||||
ContentType: 'application/pdf',
|
||||
ACL: 'private',
|
||||
})
|
||||
.promise();
|
||||
|
||||
const pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`;
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// Optional: mark previous agreements inactive
|
||||
await tx.hostAgreement.updateMany({
|
||||
const existingCount = await this.prisma.hostAgreement.count({
|
||||
where: { hostXid: host.id, isActive: true },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
await tx.hostAgreement.create({
|
||||
data: {
|
||||
hostXid: host.id,
|
||||
filePath: pdfUrl,
|
||||
versionNumber: nextVersionNumber,
|
||||
isActive: true,
|
||||
},
|
||||
const nextVersionNumber = `AG${existingCount + 1}`;
|
||||
const baseKey = `Documents/Host/${host.id}/agreements/${nextVersionNumber}`;
|
||||
|
||||
const pdfKey = `${baseKey}.pdf`;
|
||||
|
||||
await s3
|
||||
.upload({
|
||||
Bucket: config.aws.bucketName,
|
||||
Key: pdfKey,
|
||||
Body: pdfBuffer,
|
||||
ContentType: 'application/pdf',
|
||||
ACL: 'private',
|
||||
})
|
||||
.promise();
|
||||
|
||||
pdfUrl = `https://${config.aws.bucketName}.s3.${config.aws.region}.amazonaws.com/${pdfKey}`;
|
||||
} catch (error) {
|
||||
console.error('Error generating or uploading PDF:', error);
|
||||
// Continue without PDF - will return dynamic fields instead
|
||||
}
|
||||
|
||||
try {
|
||||
const existingCount = await this.prisma.hostAgreement.count({
|
||||
where: { hostXid: host.id, isActive: true },
|
||||
});
|
||||
|
||||
await tx.hostHeader.update({
|
||||
where: { id: host.id },
|
||||
data: {
|
||||
stepper: STEPPER.AGREEMENT_ACCEPTED,
|
||||
isApproved: true,
|
||||
agreementAccepted: true,
|
||||
agreementStartDate: host.agreementStartDate || new Date(),
|
||||
},
|
||||
const nextVersionNumber = `AG${existingCount + 1}`;
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// Optional: mark previous agreements inactive
|
||||
await tx.hostAgreement.updateMany({
|
||||
where: { hostXid: host.id, isActive: true },
|
||||
data: { isActive: false },
|
||||
});
|
||||
|
||||
await tx.hostAgreement.create({
|
||||
data: {
|
||||
hostXid: host.id,
|
||||
filePath: pdfUrl,
|
||||
versionNumber: nextVersionNumber,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.hostHeader.update({
|
||||
where: { id: host.id },
|
||||
data: {
|
||||
stepper: STEPPER.AGREEMENT_ACCEPTED,
|
||||
isApproved: true,
|
||||
agreementAccepted: true,
|
||||
agreementStartDate: host.agreementStartDate || new Date(),
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating host agreement record:', error);
|
||||
// Continue without creating agreement record - will return dynamic fields instead
|
||||
}
|
||||
|
||||
// Return dynamic fields and PDF URL
|
||||
return {
|
||||
filePath: pdfUrl,
|
||||
dynamicFields: agreementVars,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest (active) agreement for a specific host by hostXid.
|
||||
*/
|
||||
async getLatestHostAgreement(hostXid: number) {
|
||||
if (!hostXid || Number.isNaN(hostXid)) {
|
||||
throw new ApiError(400, 'Valid hostXid is required');
|
||||
}
|
||||
|
||||
const hostHeader = await this.prisma.hostHeader.findFirst({
|
||||
where: { id: hostXid, isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
isCommisionBase: true,
|
||||
commisionPer: true,
|
||||
durationNumber: true,
|
||||
durationFrequency: true,
|
||||
amountPerBooking: true,
|
||||
agreementStartDate: true,
|
||||
payoutDurationNum: true,
|
||||
payoutDurationFrequency: true,
|
||||
registrationNumber: true,
|
||||
companyName: true,
|
||||
companyTypes: {
|
||||
select: {
|
||||
id: true,
|
||||
companyTypeName: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const agreement = await this.prisma.hostAgreement.findFirst({
|
||||
where: { hostXid, isActive: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: {
|
||||
id: true,
|
||||
hostXid: true,
|
||||
filePath: true,
|
||||
versionNumber: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
// ❌ If both missing
|
||||
if (!agreement && !hostHeader) {
|
||||
throw new ApiError(404, 'No active agreement found for this host');
|
||||
}
|
||||
|
||||
let presignedUrl = "";
|
||||
|
||||
if (agreement?.filePath) {
|
||||
const key = agreement.filePath.startsWith('http')
|
||||
? agreement.filePath.split('.com/')[1]
|
||||
: agreement.filePath;
|
||||
|
||||
const bucket = config.aws.bucketName;
|
||||
presignedUrl = await getPresignedUrl(bucket, key);
|
||||
}
|
||||
|
||||
return {
|
||||
hostHeader: hostHeader || null,
|
||||
agreement: agreement
|
||||
? {
|
||||
...agreement,
|
||||
presignedUrl
|
||||
}
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
async getPQQQuestionDetail(question_xid: number, activity_xid: number) {
|
||||
@@ -2374,15 +2626,9 @@ export class HostService {
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
navigationModeName: true,
|
||||
isInActivityChargeable: true,
|
||||
navigationModesTotalPrice: true,
|
||||
navigationMode: {
|
||||
select: {
|
||||
id: true,
|
||||
navigationModeName: true,
|
||||
navigationModeIcon: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
equipmentAvailable: true,
|
||||
@@ -3706,7 +3952,7 @@ export class HostService {
|
||||
const navMode = await tx.activityNavigationModes.create({
|
||||
data: {
|
||||
activityXid,
|
||||
navigationModeXid: mode.navigationModeXid,
|
||||
navigationModeName: mode.navigationModeName,
|
||||
isInActivityChargeable: isChargeable,
|
||||
navigationModesBasePrice: basePrice,
|
||||
navigationModesTotalPrice: totalPrice,
|
||||
|
||||
@@ -27,15 +27,21 @@ export const handler = safeHandler(async (
|
||||
// 2) Authenticate user
|
||||
await verifyMinglarAdminHostToken(token);
|
||||
|
||||
// 3) Get bankXid from query params
|
||||
// 3) Get stateXid and optional search term from query params
|
||||
const stateXid = Number(event.queryStringParameters?.stateXid);
|
||||
const search = event.queryStringParameters?.search?.trim();
|
||||
|
||||
if (!stateXid || isNaN(stateXid)) {
|
||||
throw new ApiError(400, "Valid stateXid is required in query params.");
|
||||
}
|
||||
|
||||
// 4) Fetch branches for the bank
|
||||
const branches = await prePopulateService.getCityByStateId(stateXid);
|
||||
// If search is provided, enforce minimum 3 characters
|
||||
if (search && search.length < 3) {
|
||||
throw new ApiError(400, "Search term must be at least 3 characters long.");
|
||||
}
|
||||
|
||||
// 4) Fetch cities for the state (optionally filtered by search)
|
||||
const branches = await prePopulateService.getCityByStateId(stateXid, search);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
|
||||
@@ -39,12 +39,20 @@ export class PrePopulateService {
|
||||
}
|
||||
|
||||
|
||||
async getCityByStateId(stateXid: number) {
|
||||
async getCityByStateId(stateXid: number, search?: string) {
|
||||
return await this.prisma.cities.findMany({
|
||||
where: {
|
||||
stateXid,
|
||||
isActive: true,
|
||||
deletedAt: null
|
||||
deletedAt: null,
|
||||
...(search && search.length >= 3
|
||||
? {
|
||||
cityName: {
|
||||
contains: search,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -153,7 +161,6 @@ export class PrePopulateService {
|
||||
foodType,
|
||||
cuisineDetails,
|
||||
vehicleType,
|
||||
navigationMode,
|
||||
taxDetails,
|
||||
energyLevel,
|
||||
aminitiesDetails,
|
||||
@@ -171,9 +178,6 @@ export class PrePopulateService {
|
||||
this.prisma.transportModes.findMany({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
this.prisma.navigationModes.findMany({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
this.prisma.taxes.findMany({
|
||||
where: { isActive: true },
|
||||
}),
|
||||
@@ -215,7 +219,6 @@ export class PrePopulateService {
|
||||
foodType,
|
||||
cuisineDetails,
|
||||
vehicleType,
|
||||
navigationMode,
|
||||
taxDetails,
|
||||
energyLevel,
|
||||
aminitiesDetails,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
|
||||
import { prismaClient } from '../../../../common/database/prisma.lambda.service';
|
||||
import { verifyUserToken } from '../../../../common/middlewares/jwt/authForUser';
|
||||
import { safeHandler } from '../../../../common/utils/handlers/safeHandler';
|
||||
import ApiError from '../../../../common/utils/helper/ApiError';
|
||||
import { UserService } from '../../services/user.service';
|
||||
|
||||
const userService = new UserService(prismaClient);
|
||||
|
||||
export const handler = safeHandler(async (
|
||||
event: APIGatewayProxyEvent,
|
||||
context?: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
// Extract token from headers
|
||||
const token = event.headers['x-auth-token'] || event.headers['X-Auth-Token']
|
||||
if (!token) {
|
||||
throw new ApiError(400, 'This is a protected route. Please provide a valid token.');
|
||||
}
|
||||
|
||||
// Authenticate user using verifyUserToken
|
||||
const userInfo = await verifyUserToken(token);
|
||||
const userId = userInfo.id;
|
||||
|
||||
if (Number.isNaN(userId)) {
|
||||
throw new ApiError(400, 'User id must be a number');
|
||||
}
|
||||
|
||||
const user = await userService.getUserById(userId);
|
||||
if (!user) {
|
||||
throw new ApiError(404, 'User not found');
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
let body: { activityXid: number; isBucket: boolean; bucketTypeName: string; };
|
||||
|
||||
try {
|
||||
body = event.body ? JSON.parse(event.body) : {};
|
||||
} catch (error) {
|
||||
throw new ApiError(400, 'Invalid JSON in request body');
|
||||
}
|
||||
|
||||
const { activityXid, isBucket, bucketTypeName } = body;
|
||||
|
||||
// Validate required fields
|
||||
if (
|
||||
typeof activityXid !== 'number' ||
|
||||
typeof isBucket !== 'boolean' ||
|
||||
!bucketTypeName
|
||||
) {
|
||||
throw new ApiError(400, 'Required fields missing or invalid');
|
||||
}
|
||||
|
||||
|
||||
// Set the passcode
|
||||
const counts = await userService.addToBucketInterested(userId, isBucket, bucketTypeName, activityXid);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
success: true,
|
||||
message: `Activity added to ${isBucket ? 'bucket' : 'interested'} successfully`,
|
||||
data: {
|
||||
bucketCount: counts.bucketCount,
|
||||
interestedCount: counts.interestedCount,
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -709,6 +709,29 @@ export class UserService {
|
||||
};
|
||||
}
|
||||
|
||||
const userBucketInterested = await tx.userBucketInterested.findMany({
|
||||
where: {
|
||||
userXid: userId,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
activityXid: true,
|
||||
isBucket: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userBucketActivityIds = userBucketInterested
|
||||
.filter(u => u.isBucket)
|
||||
.map(u => u.activityXid);
|
||||
|
||||
const userInterestedActivityIds = userBucketInterested
|
||||
.filter(u => !u.isBucket)
|
||||
.map(u => u.activityXid);
|
||||
|
||||
const allUserExcludedActivityIds = userBucketInterested.map(
|
||||
u => u.activityXid,
|
||||
);
|
||||
|
||||
const userConnectionDetails = await tx.connectDetails.findMany({
|
||||
where: { userXid: userId, isActive: true },
|
||||
select: {
|
||||
@@ -762,6 +785,11 @@ export class UserService {
|
||||
activityTypeXid: {
|
||||
in: activitiyTypesOfUserInterests.map((at) => at.id),
|
||||
},
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1], // prevent empty notIn issue
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
@@ -842,7 +870,12 @@ export class UserService {
|
||||
// IF user wants the standard 4-step ranking applied TO the most hyped items:
|
||||
const mostHypedActivitiesRaw = await tx.activities.findMany({
|
||||
where: {
|
||||
id: { in: mostHypedActivityIds },
|
||||
id: {
|
||||
in: mostHypedActivityIds,
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1],
|
||||
},
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
@@ -966,6 +999,11 @@ export class UserService {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1], // prevent empty notIn issue
|
||||
},
|
||||
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
|
||||
};
|
||||
|
||||
@@ -984,6 +1022,11 @@ export class UserService {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1], // prevent empty notIn issue
|
||||
},
|
||||
};
|
||||
|
||||
if (effectiveCountryXid) {
|
||||
@@ -1010,6 +1053,11 @@ export class UserService {
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
deletedAt: null,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1], // prevent empty notIn issue
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1034,6 +1082,11 @@ export class UserService {
|
||||
amInternalStatus:
|
||||
ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
deletedAt: null,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1], // prevent empty notIn issue
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -1076,6 +1129,11 @@ export class UserService {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
id: {
|
||||
notIn: allUserExcludedActivityIds.length
|
||||
? allUserExcludedActivityIds
|
||||
: [-1], // prevent empty notIn issue
|
||||
},
|
||||
};
|
||||
|
||||
if (effectiveCountryXid) {
|
||||
@@ -1152,6 +1210,8 @@ export class UserService {
|
||||
loggedInNetworkCount: 0,
|
||||
citiesInNetworkCount: 0,
|
||||
rating: 0,
|
||||
interestedCount: userInterestedActivityIds.length,
|
||||
bucketCount: userBucketActivityIds.length,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
@@ -1237,6 +1297,32 @@ export class UserService {
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const userBucketInterested = await tx.userBucketInterested.findMany({
|
||||
where: {
|
||||
userXid: userId,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
activityXid: true,
|
||||
isBucket: true,
|
||||
},
|
||||
});
|
||||
|
||||
const bucketActivityIds = userBucketInterested
|
||||
.filter(u => u.isBucket)
|
||||
.map(u => u.activityXid);
|
||||
|
||||
const interestedActivityIds = userBucketInterested
|
||||
.filter(u => !u.isBucket)
|
||||
.map(u => u.activityXid);
|
||||
|
||||
const excludedActivityIds = userBucketInterested.map(
|
||||
u => u.activityXid,
|
||||
);
|
||||
|
||||
const safeExcludedIds =
|
||||
excludedActivityIds.length > 0 ? excludedActivityIds : [-1];
|
||||
|
||||
/* =====================================================
|
||||
CONNECTION INTEREST MAP
|
||||
===================================================== */
|
||||
@@ -1270,7 +1356,6 @@ export class UserService {
|
||||
where: {
|
||||
userXid: { in: connectionUserIds },
|
||||
isActive: true,
|
||||
isBucket: true,
|
||||
},
|
||||
_count: { activityXid: true },
|
||||
});
|
||||
@@ -1302,6 +1387,9 @@ export class UserService {
|
||||
const otherInterestActivities = await tx.activities.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
id: { notIn: safeExcludedIds },
|
||||
...excludeUserInterestCondition,
|
||||
},
|
||||
skip,
|
||||
@@ -1388,7 +1476,16 @@ export class UserService {
|
||||
).length;
|
||||
|
||||
const hypedActivities = await tx.activities.findMany({
|
||||
where: { id: { in: mostHypedGrouped.map((h) => h.activityXid) } },
|
||||
where: {
|
||||
id: {
|
||||
in: mostHypedGrouped.map((h) => h.activityXid),
|
||||
notIn: safeExcludedIds,
|
||||
},
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
isActive: true,
|
||||
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
activityTitle: true,
|
||||
@@ -1427,7 +1524,10 @@ export class UserService {
|
||||
5️⃣ NEW ARRIVALS
|
||||
===================================================== */
|
||||
const newArrivalsWhere = {
|
||||
id: { notIn: safeExcludedIds },
|
||||
isActive: true,
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
createdAt: { gte: new Date(Date.now() - 31 * 24 * 60 * 60 * 1000) },
|
||||
...excludeUserInterestCondition,
|
||||
};
|
||||
@@ -1457,6 +1557,9 @@ export class UserService {
|
||||
===================================================== */
|
||||
const otherStatesWhere: any = {
|
||||
isActive: true,
|
||||
id: { notIn: safeExcludedIds },
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
...excludeUserInterestCondition,
|
||||
};
|
||||
if (effectiveCountryXid)
|
||||
@@ -1466,6 +1569,9 @@ export class UserService {
|
||||
|
||||
const overseasWhere: any = {
|
||||
isActive: true,
|
||||
id: { notIn: safeExcludedIds },
|
||||
activityInternalStatus: ACTIVITY_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
amInternalStatus: ACTIVITY_AM_INTERNAL_STATUS.ACTIVITY_LISTED,
|
||||
...excludeUserInterestCondition,
|
||||
};
|
||||
if (effectiveCountryXid)
|
||||
@@ -1513,6 +1619,8 @@ export class UserService {
|
||||
return {
|
||||
pagination: { page, limit },
|
||||
interests: interestsWithActivities,
|
||||
interestedCount: interestedActivityIds.length,
|
||||
bucketCount: bucketActivityIds.length,
|
||||
|
||||
mostHypedActivities: {
|
||||
page,
|
||||
@@ -1743,14 +1851,7 @@ export class UserService {
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
navigationModeXid: true,
|
||||
navigationMode: {
|
||||
select: {
|
||||
id: true,
|
||||
navigationModeName: true,
|
||||
navigationModeIcon: true,
|
||||
},
|
||||
},
|
||||
navigationModeName: true,
|
||||
isInActivityChargeable: true,
|
||||
navigationModesTotalPrice: true,
|
||||
},
|
||||
@@ -1951,13 +2052,6 @@ export class UserService {
|
||||
|
||||
const connectionUserIds = connectionUsers.map((u) => u.userXid);
|
||||
|
||||
const interestedCount = await tx.userBucketInterested.count({
|
||||
where: {
|
||||
activityXid,
|
||||
isBucket: false,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const connectionInterestedCount = connectionUserIds.length
|
||||
? await tx.userBucketInterested.count({
|
||||
@@ -1979,6 +2073,50 @@ export class UserService {
|
||||
(v) => v.venueCapacity ?? 0,
|
||||
).reduce((sum, capacity) => sum + capacity, 0);
|
||||
|
||||
const interestedCount = await tx.userBucketInterested.count({
|
||||
where: {
|
||||
activityXid,
|
||||
isBucket: false,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const interestedUsers = await tx.userBucketInterested.findMany({
|
||||
where: {
|
||||
activityXid,
|
||||
isBucket: false,
|
||||
isActive: true,
|
||||
user: {
|
||||
isActive: true,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
profileImage: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const shuffledUsers = interestedUsers.sort(() => 0.5 - Math.random());
|
||||
const randomFive = shuffledUsers.slice(0, 5);
|
||||
|
||||
const interestedUserImages: string[] = [];
|
||||
|
||||
for (const item of randomFive) {
|
||||
const profileImage = item.user.profileImage;
|
||||
|
||||
if (profileImage) {
|
||||
const key = profileImage.startsWith('http')
|
||||
? new URL(profileImage).pathname.replace(/^\/+/, '')
|
||||
: profileImage;
|
||||
|
||||
const presignedUrl = await getPresignedUrl(bucket, key);
|
||||
interestedUserImages.push(presignedUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activity,
|
||||
interestedCount,
|
||||
@@ -1987,6 +2125,7 @@ export class UserService {
|
||||
totalCapacity,
|
||||
rating: 0, // ⭐ Placeholder, implement rating logic as needed
|
||||
distance: 0,
|
||||
interestedUserImages
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -2522,6 +2661,7 @@ export class UserService {
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!networkUsers.length) {
|
||||
return {
|
||||
interests: [],
|
||||
@@ -3500,4 +3640,59 @@ export class UserService {
|
||||
});
|
||||
}
|
||||
|
||||
async addToBucketInterested(
|
||||
userXid: number,
|
||||
isBucket: boolean,
|
||||
bucketTypeName: string,
|
||||
activityXid: number
|
||||
) {
|
||||
const activityExists = await this.prisma.activities.findFirst({
|
||||
where: { id: activityXid, isActive: true },
|
||||
});
|
||||
|
||||
if (!activityExists) {
|
||||
throw new ApiError(404, 'Activity not found');
|
||||
}
|
||||
|
||||
const existing = await this.prisma.userBucketInterested.findFirst({
|
||||
where: { userXid, activityXid },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new ApiError(400, 'Activity already added');
|
||||
}
|
||||
|
||||
await this.prisma.userBucketInterested.create({
|
||||
data: {
|
||||
userXid,
|
||||
activityXid,
|
||||
isBucket,
|
||||
bucketTypeName,
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ Get updated counts
|
||||
const [bucketCount, interestedCount] = await Promise.all([
|
||||
this.prisma.userBucketInterested.count({
|
||||
where: {
|
||||
userXid,
|
||||
isBucket: true,
|
||||
isActive: true,
|
||||
},
|
||||
}),
|
||||
this.prisma.userBucketInterested.count({
|
||||
where: {
|
||||
userXid,
|
||||
isBucket: false,
|
||||
isActive: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
bucketCount,
|
||||
interestedCount,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user