From 6f8fc0f38b1e937a87b3550521984211502d8420 Mon Sep 17 00:00:00 2001 From: Samuel Theophilus <105337973+afkzoro@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:28:44 +0100 Subject: [PATCH] feature(drivers-service): create fleet driver earnings + type update (#532) * feature(drivers-service): create fleet driver earnings + type update * feature: implement get all drivers payouts --- .../src/fleet/fleet.controller.ts | 26 ++++++ .../src/fleet/fleet.service.ts | 45 ++++++++- .../src/test/stub/orders.stub.ts | 5 +- .../fleets-payout/fleets-payout.controller.ts | 51 ++++++++++ .../fleets-payout.respository.ts | 15 +++ .../fleets-payout/fleets-payout.service.ts | 93 +++++++++++++++++++ apps/payment-service/src/payment.module.ts | 20 +++- .../database/schemas/fleet-payout.schema.ts | 32 +++++++ .../src/database/schemas/order.schema.ts | 6 +- libs/common/src/database/types/common.ts | 4 +- libs/common/src/dto/PlaceOrder.dto.ts | 2 +- libs/common/src/index.ts | 1 + libs/common/src/typings/Global.Interface.ts | 2 + libs/common/src/typings/QUEUE_MESSAGE.ts | 2 + .../sticky/libs/common/src/schemas/index.ts | 4 +- 15 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 apps/payment-service/src/fleets-payout/fleets-payout.controller.ts create mode 100644 apps/payment-service/src/fleets-payout/fleets-payout.respository.ts create mode 100644 apps/payment-service/src/fleets-payout/fleets-payout.service.ts create mode 100644 libs/common/src/database/schemas/fleet-payout.schema.ts diff --git a/apps/drivers-service/src/fleet/fleet.controller.ts b/apps/drivers-service/src/fleet/fleet.controller.ts index 5ca069a6..bf0425ee 100644 --- a/apps/drivers-service/src/fleet/fleet.controller.ts +++ b/apps/drivers-service/src/fleet/fleet.controller.ts @@ -4,6 +4,7 @@ import { AcceptFleetInviteDto, CreateAccountWithOrganizationDto, CurrentUser, Delivery, Driver, DriverStatGroup, FleetMember, FleetOrganization, + FleetPayout, RegisterDriverDto, ResponseWithStatus, UpdateFleetMemberProfileDto, UpdateFleetOwnershipStatusDto } from '@app/common' @@ -201,4 +202,29 @@ export class FleetController { throw new HttpException(error.message, error.status) } } + + @UseGuards(FleetJwtAuthGuard) + @Get('payout/:driverId') + async getDriverPayout ( + @CurrentUser() member: FleetMember, + @Param('driverId') driverId: string + ): Promise { + try { + return await this.fleetService.getDriverPayout(member.organization, driverId) + } catch (error) { + throw new HttpException(error.message, error.status) + } + } + + @UseGuards(FleetJwtAuthGuard) + @Get('payout/all') + async getAllDriverPayout ( + @CurrentUser() member: FleetMember + ): Promise { + try { + return await this.fleetService.getAllDriversPayout(member.organization) + } catch (error) { + throw new HttpException(error.message, error.status) + } + } } diff --git a/apps/drivers-service/src/fleet/fleet.service.ts b/apps/drivers-service/src/fleet/fleet.service.ts index 5d5ba74e..6204e19b 100644 --- a/apps/drivers-service/src/fleet/fleet.service.ts +++ b/apps/drivers-service/src/fleet/fleet.service.ts @@ -1,4 +1,4 @@ -import { HttpStatus, Injectable, Logger, NotFoundException } from '@nestjs/common' +import { HttpException, HttpStatus, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common' import { AcceptFleetInviteDto, CreateAccountWithOrganizationDto, @@ -6,8 +6,12 @@ import { Driver, DriverStatGroup, FitRpcException, FleetMember, FleetOrganization, + FleetPayout, internationalisePhoneNumber, + IRpcException, OrderStatus, + QUEUE_MESSAGE, + QUEUE_SERVICE, RandomGen, RegisterDriverDto, ResponseWithStatus, SOCKET_MESSAGE, UpdateFleetMemberProfileDto, UpdateFleetOwnershipStatusDto, @@ -22,6 +26,8 @@ import { DriversServiceService } from '../drivers-service.service' import { DriverRepository } from '../drivers-service.repository' import { OdsaRepository } from '../ODSA/odsa.repository' import { ODSA } from '../ODSA/odsa.service' +import { ClientProxy } from '@nestjs/microservices' +import { lastValueFrom, catchError } from 'rxjs' @Injectable() export class FleetService { @@ -34,7 +40,10 @@ export class FleetService { private readonly driverRepository: DriverRepository, private readonly eventsGateway: EventsGateway, private readonly odsaRepository: OdsaRepository, - private readonly odsaService: ODSA + private readonly odsaService: ODSA, + + @Inject(QUEUE_SERVICE.PAYMENT_SERVICE) + private readonly paymentClient: ClientProxy ) {} public async getProfile (id: string): Promise { @@ -454,6 +463,38 @@ export class FleetService { return await this.odsaService.getDriverStats(driverId) } + async getDriverPayout (organization: string, driverId: string): Promise { + const checkDriver = await this.driverRepository.findOne({ + _id: driverId, + organization + }) + + if (checkDriver === null) { + throw new FitRpcException( + 'Driver not found', + HttpStatus.NOT_FOUND + ) + } + + return await lastValueFrom( + this.paymentClient.send(QUEUE_MESSAGE.FLEET_GET_PAYOUT_DRIVER, { driverId }).pipe( + catchError((error: IRpcException) => { + throw new HttpException(error.message, error.status) + }) + ) + ) + } + + async getAllDriversPayout (organization: string): Promise { + return await lastValueFrom( + this.paymentClient.send(QUEUE_MESSAGE.FLEET_GET_ALL_PAYOUTS, { organization }).pipe( + catchError((error: IRpcException) => { + throw new HttpException(error.message, error.status) + }) + ) + ) + } + // @Crons /** diff --git a/apps/orders-service/src/test/stub/orders.stub.ts b/apps/orders-service/src/test/stub/orders.stub.ts index a51feecc..0d32e1b2 100644 --- a/apps/orders-service/src/test/stub/orders.stub.ts +++ b/apps/orders-service/src/test/stub/orders.stub.ts @@ -9,7 +9,7 @@ import { Types } from 'mongoose' export function OrderStub (): Order { const objectId = '63f93c9f248f6c43d0b76502' as unknown as Types.ObjectId return { - pickupAddress: "", + pickupAddress: '', precisePickupLocation: { type: 'Point', coordinates: [12.34, 56.78] @@ -53,7 +53,8 @@ export function OrderStub (): Order { deliveryFee: 10.0, vat: 12.0 }, - txRefId: 'transaction_reference_123' + txRefId: 'transaction_reference_123', + fleetOrderType: 'GROCERIES' } } diff --git a/apps/payment-service/src/fleets-payout/fleets-payout.controller.ts b/apps/payment-service/src/fleets-payout/fleets-payout.controller.ts new file mode 100644 index 00000000..14d02525 --- /dev/null +++ b/apps/payment-service/src/fleets-payout/fleets-payout.controller.ts @@ -0,0 +1,51 @@ +import { + FleetPayout, + QUEUE_MESSAGE, + RmqService + +} from '@app/common' +import { Controller } from '@nestjs/common' +import { + Ctx, + MessagePattern, + Payload, + RmqContext, + RpcException +} from '@nestjs/microservices' +import { FleetPayoutService } from './fleets-payout.service' + +@Controller() +export class FleetPayoutController { + constructor ( + private readonly fleetPayoutService: FleetPayoutService, + private readonly rmqService: RmqService + ) {} + + @MessagePattern(QUEUE_MESSAGE.FLEET_GET_PAYOUT_DRIVER) + async getDrivePayout ( + @Payload() { driverId }: { driverId: string }, + @Ctx() context: RmqContext + ): Promise { + try { + return await this.fleetPayoutService.getDriverPayout(driverId) + } catch (error) { + throw new RpcException(error) + } finally { + this.rmqService.ack(context) + } + } + + @MessagePattern(QUEUE_MESSAGE.FLEET_GET_ALL_PAYOUTS) + async getAllDriversPayout ( + @Payload() { organization }: { organization: string }, + @Ctx() context: RmqContext + ): Promise { + try { + return await this.fleetPayoutService.getAllDriversPayout(organization) + } catch (error) { + throw new RpcException(error) + } finally { + this.rmqService.ack(context) + } + } +} diff --git a/apps/payment-service/src/fleets-payout/fleets-payout.respository.ts b/apps/payment-service/src/fleets-payout/fleets-payout.respository.ts new file mode 100644 index 00000000..4c85b426 --- /dev/null +++ b/apps/payment-service/src/fleets-payout/fleets-payout.respository.ts @@ -0,0 +1,15 @@ +import { Injectable, Logger } from '@nestjs/common' +import { AbstractRepository, FleetPayout } from '@app/common' +import { InjectModel } from '@nestjs/mongoose' +import { Model } from 'mongoose' + +@Injectable() +export class FleetPayoutRepository extends AbstractRepository { + protected readonly logger = new Logger(FleetPayout.name) + + constructor ( + @InjectModel(FleetPayout.name) payoutModel: Model + ) { + super(payoutModel) + } +} diff --git a/apps/payment-service/src/fleets-payout/fleets-payout.service.ts b/apps/payment-service/src/fleets-payout/fleets-payout.service.ts new file mode 100644 index 00000000..8501c0db --- /dev/null +++ b/apps/payment-service/src/fleets-payout/fleets-payout.service.ts @@ -0,0 +1,93 @@ +import { OrderStatus, QUEUE_MESSAGE, IRpcException, FitRpcException, RandomGen, FleetPayout, QUEUE_SERVICE, DeliveryI, Delivery, DriverI } from '@app/common' +import { Inject, Injectable } from '@nestjs/common' +import { ClientProxy } from '@nestjs/microservices' +import { FilterQuery } from 'mongoose' +import { lastValueFrom, catchError } from 'rxjs' +import { FleetPayoutRepository } from './fleets-payout.respository' +import { Cron, CronExpression } from '@nestjs/schedule' +import { DriverRepository } from 'apps/drivers-service/src/drivers-service.repository' + +@Injectable() +export class FleetPayoutService { + constructor ( + private readonly fleetPayoutRepository: FleetPayoutRepository, + private readonly driverRepository: DriverRepository, + @Inject(QUEUE_SERVICE.DRIVER_SERVICE) + private readonly driverClient: ClientProxy + + ) { + + } + + async getDriverPayout (driver: string): Promise { + return await this.fleetPayoutRepository.findOneAndPopulate({ driver }, ['deliveries']) + } + + async getAllDriversPayout (organization: string): Promise { + const drivers: DriverI[] = await this.driverRepository.find({ organization }) + + const driverIds = drivers.map((driver) => driver._id) + + return await this.fleetPayoutRepository.findAndPopulate( + { driver: { $in: driverIds } }, + ['deliveries'] + ) + } + + @Cron(CronExpression.EVERY_DAY_AT_10PM, { + timeZone: 'Africa/Lagos' + }) + async handlePayoutComputation (): Promise { + const today = new Date() + const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000) + + const start = new Date(yesterday) + start.setHours(0, 0, 0, 0) + + const end = new Date(yesterday) + end.setHours(23, 59, 59, 999) + + const filter: FilterQuery = { + createdAt: { + $gte: start.toISOString(), + $lt: end.toISOString() + }, + status: OrderStatus.FULFILLED + } + const deliveries = await lastValueFrom( + this.driverClient.send(QUEUE_MESSAGE.ADMIN_GET_DELIVERIES, filter).pipe( + catchError((error: IRpcException) => { + throw new FitRpcException(error.message, error.status) + }) + ) + ) + + const deliveryId = deliveries.map((delivery) => delivery._id) + + // Compute earnings for each driver + const driverEarnings = new Map() + + deliveries.forEach((delivery) => { + const driverId = delivery.driver._id.toString() + const earnings = driverEarnings.get(driverId) ?? 0 + driverEarnings.set( + driverId, + earnings + Number(delivery.deliveryFee) + ) + }) + + const payoutsToMake: Array> = [] + + for (const [driverId, earnings] of driverEarnings) { + payoutsToMake.push({ + refId: RandomGen.genRandomNum(10, 7), + driver: driverId, + earnings, + paid: false, + deliveries: deliveryId + }) + } + + await this.fleetPayoutRepository.insertMany(payoutsToMake) + } +} diff --git a/apps/payment-service/src/payment.module.ts b/apps/payment-service/src/payment.module.ts index 1f236211..f952eb21 100644 --- a/apps/payment-service/src/payment.module.ts +++ b/apps/payment-service/src/payment.module.ts @@ -23,7 +23,9 @@ import { Coupon, CouponSchema, ListingMenu, - ListingMenuSchema + ListingMenuSchema, + FleetPayout, + FleetPayoutSchema } from '@app/common' import { ScheduleModule } from '@nestjs/schedule' import { PaymentRepository } from './charge/charge.repository' @@ -51,6 +53,9 @@ import { UserWalletController } from './wallet/user/wallet.controller' import { UserWalletService } from './wallet/user/wallet.service' import { UserWallet, UserWalletSchema } from '@app/common/database/schemas/user-wallet.schema' import { UserWalletRepository } from './wallet/user/wallet.repository' +import { FleetPayoutService } from './fleets-payout/fleets-payout.service' +import { FleetPayoutRepository } from './fleets-payout/fleets-payout.respository' +import { FleetPayoutController } from './fleets-payout/fleets-payout.controller' @Module({ imports: [ @@ -95,6 +100,12 @@ import { UserWalletRepository } from './wallet/user/wallet.repository' schema: ListingMenuSchema } ]), + MongooseModule.forFeature([ + { + name: FleetPayout.name, + schema: FleetPayoutSchema + } + ]), MongooseModule.forFeature([ { name: Payment.name, @@ -132,7 +143,8 @@ import { UserWalletRepository } from './wallet/user/wallet.repository' PaymentController, DriverWalletController, UserWalletController, - CouponController + CouponController, + FleetPayoutController ], providers: [ VendorPayoutService, @@ -147,7 +159,9 @@ import { UserWalletRepository } from './wallet/user/wallet.repository' CouponService, CouponRepository, UserWalletService, - UserWalletRepository + UserWalletRepository, + FleetPayoutService, + FleetPayoutRepository ] }) export class PaymentServiceModule {} diff --git a/libs/common/src/database/schemas/fleet-payout.schema.ts b/libs/common/src/database/schemas/fleet-payout.schema.ts new file mode 100644 index 00000000..7204189a --- /dev/null +++ b/libs/common/src/database/schemas/fleet-payout.schema.ts @@ -0,0 +1,32 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' +import { AbstractDocument } from '../abstract.schema' +import { Types } from 'mongoose' + +@Schema({ versionKey: false, timestamps: true }) +export class FleetPayout extends AbstractDocument { + @Prop({ + type: Types.ObjectId, + ref: 'Driver' + }) + public driver: string + + @Prop(Number) + earnings: number + + @Prop({ type: Boolean, default: false }) + paid: boolean + + @Prop(Date) + updatedAt: string + + @Prop(Date) + createdAt: string + + @Prop({ type: [Types.ObjectId], ref: 'Delivery' }) + deliveries: string[] + + @Prop(Number) + refId: number +} + +export const FleetPayoutSchema = SchemaFactory.createForClass(FleetPayout) diff --git a/libs/common/src/database/schemas/order.schema.ts b/libs/common/src/database/schemas/order.schema.ts index 1d5ffcd8..9319153a 100644 --- a/libs/common/src/database/schemas/order.schema.ts +++ b/libs/common/src/database/schemas/order.schema.ts @@ -6,7 +6,8 @@ import { OrderBreakDown, OrderType, LocationCoordinates, - OrderOptions + OrderOptions, + FleetOrderType } from '@app/common' @Schema({ versionKey: false, timestamps: true }) @@ -154,6 +155,9 @@ export class Order extends AbstractDocument { @Prop(Number) pin_code: number + + @Prop(String) + fleetOrderType: FleetOrderType } export const OrderSchema = SchemaFactory.createForClass(Order) diff --git a/libs/common/src/database/types/common.ts b/libs/common/src/database/types/common.ts index 66a42051..d1cedc82 100644 --- a/libs/common/src/database/types/common.ts +++ b/libs/common/src/database/types/common.ts @@ -3,7 +3,8 @@ import { VendorOperationType, OrderStatus, DriverI, - CouponType + CouponType, + FleetOrderType } from '@app/common' import { ListingApprovalStatus } from '@app/common/typings/ListingApprovalStatus.enum' @@ -201,6 +202,7 @@ export interface OrderI { coupon?: string review?: ReviewI + fleetOrderType: FleetOrderType } export interface ListingMenuReview { diff --git a/libs/common/src/dto/PlaceOrder.dto.ts b/libs/common/src/dto/PlaceOrder.dto.ts index c59c66f0..8a114f43 100644 --- a/libs/common/src/dto/PlaceOrder.dto.ts +++ b/libs/common/src/dto/PlaceOrder.dto.ts @@ -65,7 +65,7 @@ export class PlaceOrderDto { @IsString() @IsOptional() - specialNote: string + specialNote: string @IsString() pickupAddress: string diff --git a/libs/common/src/index.ts b/libs/common/src/index.ts index d7f39dee..aaf0ba9d 100644 --- a/libs/common/src/index.ts +++ b/libs/common/src/index.ts @@ -46,6 +46,7 @@ export * from './database/types/common' export * from './database/schemas/vendor-payout' export * from './database/schemas/fleet-organization.schema' export * from './database/schemas/fleet-member.schema' +export * from './database/schemas/fleet-payout.schema' export * from './typings/Payment' export * from './utils/env_check' export * from './utils/nairaconverter' diff --git a/libs/common/src/typings/Global.Interface.ts b/libs/common/src/typings/Global.Interface.ts index 97953986..567b8567 100644 --- a/libs/common/src/typings/Global.Interface.ts +++ b/libs/common/src/typings/Global.Interface.ts @@ -137,6 +137,8 @@ export interface DeliveryI { distance: number travelTime: number } + + deliveryFee: number } export interface DriverI { diff --git a/libs/common/src/typings/QUEUE_MESSAGE.ts b/libs/common/src/typings/QUEUE_MESSAGE.ts index 8cdbd9e8..faebeb3b 100644 --- a/libs/common/src/typings/QUEUE_MESSAGE.ts +++ b/libs/common/src/typings/QUEUE_MESSAGE.ts @@ -172,6 +172,8 @@ export enum QUEUE_MESSAGE { ODSA_GET_ORDERS_PRE = 'ODSA_GET_ORDERS_PRE', STREAM_ORDER_UPDATES = 'STREAM_ORDER_UPDATES', + FLEET_GET_PAYOUT_DRIVER = 'FLEET_GET_PAYOUT_DRIVER', + FLEET_GET_ALL_PAYOUTS = 'FLEET_GET_ALL_PAYOUTS', LOCATION_GET_ETA = 'LOCATION_GET_ETA', LOCATION_GET_DELIVERY_FEE = 'LOCATION_GET_DELIVERY_FEE', diff --git a/packages/sticky/libs/common/src/schemas/index.ts b/packages/sticky/libs/common/src/schemas/index.ts index 299340f7..f4f858b6 100644 --- a/packages/sticky/libs/common/src/schemas/index.ts +++ b/packages/sticky/libs/common/src/schemas/index.ts @@ -6,7 +6,8 @@ import { PaymentInfo, VendorOperationSetting, DriverType, AdminLevel, ListingApprovalStatus, OrderOptions, - VendorApprovalStatusEnum, + VendorApprovalStatusEnum, + FleetOrderType, } from '../typings' export interface ListingCategoryI { _id: string @@ -172,6 +173,7 @@ export interface OrderI { coupon?: string review?: ReviewI + fleetOrderType: FleetOrderType } export interface DeliveryI {