diff --git a/api/.env.docker.example b/api/.env.docker.example
index cc35f55ff..15a1ce243 100644
--- a/api/.env.docker.example
+++ b/api/.env.docker.example
@@ -24,6 +24,8 @@ BC_CDN_CARS=/var/www/cdn/bookcars/cars
BC_CDN_TEMP_CARS=/var/www/cdn/bookcars/temp/cars
BC_CDN_LOCATIONS=/var/www/cdn/bookcars/locations
BC_CDN_TEMP_LOCATIONS=/var/www/cdn/bookcars/temp/locations
+BC_CDN_CONTRACTS=/var/www/cdn/bookcars/contracts
+BC_CDN_TEMP_CONTRACTS=/var/www/cdn/bookcars/temp/contracts
BC_DEFAULT_LANGUAGE=en
BC_BACKEND_HOST=http://localhost:3001/
BC_FRONTEND_HOST=http://localhost/
diff --git a/api/.env.example b/api/.env.example
index 0030fa868..cd016e30d 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -24,6 +24,8 @@ BC_CDN_CARS=/var/www/cdn/bookcars/cars
BC_CDN_TEMP_CARS=/var/www/cdn/bookcars/temp/cars
BC_CDN_LOCATIONS=/var/www/cdn/bookcars/locations
BC_CDN_TEMP_LOCATIONS=/var/www/cdn/bookcars/temp/locations
+BC_CDN_CONTRACTS=/var/www/cdn/bookcars/contracts
+BC_CDN_TEMP_CONTRACTS=/var/www/cdn/bookcars/temp/contracts
BC_DEFAULT_LANGUAGE=en
BC_BACKEND_HOST=http://localhost:3001/
BC_FRONTEND_HOST=http://localhost:3002/
diff --git a/api/src/app.ts b/api/src/app.ts
index 0ea5f882a..6929a7595 100644
--- a/api/src/app.ts
+++ b/api/src/app.ts
@@ -61,5 +61,7 @@ await helper.mkdir(env.CDN_CARS)
await helper.mkdir(env.CDN_TEMP_CARS)
await helper.mkdir(env.CDN_LOCATIONS)
await helper.mkdir(env.CDN_TEMP_LOCATIONS)
+await helper.mkdir(env.CDN_CONTRACTS)
+await helper.mkdir(env.CDN_TEMP_CONTRACTS)
export default app
diff --git a/api/src/config/env.config.ts b/api/src/config/env.config.ts
index f4342369b..4b9b0b45b 100644
--- a/api/src/config/env.config.ts
+++ b/api/src/config/env.config.ts
@@ -245,6 +245,20 @@ export const CDN_LOCATIONS = __env__('BC_CDN_LOCATIONS', true)
*/
export const CDN_TEMP_LOCATIONS = __env__('BC_CDN_TEMP_LOCATIONS', true)
+/**
+ * Contracts' cdn folder path.
+ *
+ * @type {string}
+ */
+export const CDN_CONTRACTS = __env__('BC_CDN_CONTRACTS', true)
+
+/**
+ * Contracts' temp cdn folder path.
+ *
+ * @type {string}
+ */
+export const CDN_TEMP_CONTRACTS = __env__('BC_CDN_TEMP_CONTRACTS', true)
+
/**
* Backend host.
*
@@ -358,6 +372,7 @@ export interface User extends Document {
blacklisted?: boolean
payLater?: boolean
customerId?: string
+ contracts?: bookcarsTypes.Contract[]
expireAt?: Date
}
diff --git a/api/src/config/supplierRoutes.config.ts b/api/src/config/supplierRoutes.config.ts
index 123bfcfb5..704631db9 100644
--- a/api/src/config/supplierRoutes.config.ts
+++ b/api/src/config/supplierRoutes.config.ts
@@ -7,6 +7,10 @@ const routes = {
getAllSuppliers: '/api/all-suppliers',
getFrontendSuppliers: '/api/frontend-suppliers',
getBackendSuppliers: '/api/backend-suppliers',
+ createContract: '/api/create-contract/:language',
+ updateContract: '/api/update-contract/:id/:language',
+ deleteContract: '/api/delete-contract/:id/:language',
+ deleteTempContract: '/api/delete-temp-contract/:file',
}
export default routes
diff --git a/api/src/controllers/bookingController.ts b/api/src/controllers/bookingController.ts
index c51c99602..437383de2 100644
--- a/api/src/controllers/bookingController.ts
+++ b/api/src/controllers/bookingController.ts
@@ -3,6 +3,7 @@ import escapeStringRegexp from 'escape-string-regexp'
import { Expo, ExpoPushMessage, ExpoPushTicket } from 'expo-server-sdk'
import { Request, Response } from 'express'
import nodemailer from 'nodemailer'
+import path from 'node:path'
import * as bookcarsTypes from ':bookcars-types'
import i18n from '../lang/i18n'
import Booking from '../models/Booking'
@@ -106,7 +107,7 @@ export const notify = async (driver: env.User, bookingId: string, user: env.User
* @param {boolean} payLater
* @returns {unknown}
*/
-export const confirm = async (user: env.User, booking: env.Booking, payLater: boolean) => {
+export const confirm = async (user: env.User, supplier: env.User, booking: env.Booking, payLater: boolean = false) => {
const { language } = user
const locale = language === 'fr' ? 'fr-FR' : 'en-US'
const options: Intl.DateTimeFormatOptions = {
@@ -138,6 +139,14 @@ export const confirm = async (user: env.User, booking: env.Booking, payLater: bo
}
const dropOffLocationName = dropOffLocation.values.filter((value) => value.language === language)[0].value
+ let contractFile: string | null = null
+ if (supplier.contracts) {
+ contractFile = supplier.contracts.find((c) => c.language === user.language)?.file || null
+ if (!contractFile) {
+ contractFile = supplier.contracts.find((c) => c.language === 'en')?.file || null
+ }
+ }
+
const mailOptions: nodemailer.SendMailOptions = {
from: env.SMTP_FROM,
to: user.email,
@@ -158,6 +167,13 @@ export const confirm = async (user: env.User, booking: env.Booking, payLater: bo
`,
}
+ if (contractFile) {
+ const file = path.join(env.CDN_CONTRACTS, contractFile)
+ if (await helper.exists(file)) {
+ mailOptions.attachments = [{ path: file }]
+ }
+ }
+
await mailHelper.sendMail(mailOptions)
return true
@@ -179,7 +195,7 @@ export const checkout = async (req: Request, res: Response) => {
const { driver } = body
if (!body.booking) {
- throw new Error('Booking missing')
+ throw new Error('Booking not found')
}
if (driver) {
@@ -214,17 +230,14 @@ export const checkout = async (req: Request, res: Response) => {
}
if (!user) {
- logger.info('Driver not found', body)
- return res.sendStatus(204)
+ throw new Error('Driver not found')
}
if (!body.payLater) {
const { paymentIntentId, sessionId } = body
if (!paymentIntentId && !sessionId) {
- const message = 'Payment intent and session missing'
- logger.error(message, body)
- return res.status(400).send(message)
+ throw new Error('paymentIntentId and sessionId not found')
}
body.booking.customerId = body.customerId
@@ -288,43 +301,38 @@ export const checkout = async (req: Request, res: Response) => {
if (booking.status === bookcarsTypes.BookingStatus.Paid && body.paymentIntentId && body.customerId) {
const car = await Car.findById(booking.car)
if (!car) {
- logger.info(`Car ${booking.car} not found`)
- return res.sendStatus(204)
+ throw new Error(`Car ${booking.car} not found`)
}
car.trips += 1
await car.save()
+ }
- if (!await confirm(user, booking, false)) {
- return res.sendStatus(400)
+ if (body.payLater || (booking.status === bookcarsTypes.BookingStatus.Paid && body.paymentIntentId && body.customerId)) {
+ const supplier = await User.findById(booking.supplier)
+ if (!supplier) {
+ throw new Error(`Supplier ${booking.supplier} not found`)
}
- }
- if (body.payLater) {
- // Send confirmation email
- if (!await confirm(user, booking, body.payLater)) {
+ // Send confirmation email to customer
+ if (!await confirm(user, supplier, booking, body.payLater)) {
return res.sendStatus(400)
}
// Notify supplier
- const supplier = await User.findById(booking.supplier)
- if (!supplier) {
- logger.info(`Supplier ${booking.supplier} not found`)
- return res.sendStatus(204)
- }
i18n.locale = supplier.language
let message = body.payLater ? i18n.t('BOOKING_PAY_LATER_NOTIFICATION') : i18n.t('BOOKING_PAID_NOTIFICATION')
- await notify(user, booking._id.toString(), supplier, message)
+ await notify(user, booking.id, supplier, message)
// Notify admin
const admin = !!env.ADMIN_EMAIL && await User.findOne({ email: env.ADMIN_EMAIL, type: bookcarsTypes.UserType.Admin })
if (admin) {
i18n.locale = admin.language
message = body.payLater ? i18n.t('BOOKING_PAY_LATER_NOTIFICATION') : i18n.t('BOOKING_PAID_NOTIFICATION')
- await notify(user, booking._id.toString(), admin, message)
+ await notify(user, booking.id, admin, message)
}
}
- return res.status(200).send({ bookingId: booking._id })
+ return res.status(200).send({ bookingId: booking.id })
} catch (err) {
logger.error(`[booking.checkout] ${i18n.t('ERROR')}`, err)
return res.status(400).send(i18n.t('ERROR') + err)
diff --git a/api/src/controllers/locationController.ts b/api/src/controllers/locationController.ts
index bd1688406..4464895f0 100644
--- a/api/src/controllers/locationController.ts
+++ b/api/src/controllers/locationController.ts
@@ -707,12 +707,10 @@ export const deleteTempImage = async (req: Request, res: Response) => {
try {
const imageFile = path.join(env.CDN_TEMP_LOCATIONS, image)
- if (!await helper.exists(imageFile)) {
- throw new Error(`[location.deleteTempImage] temp image ${imageFile} not found`)
+ if (await helper.exists(imageFile)) {
+ await fs.unlink(imageFile)
}
- await fs.unlink(imageFile)
-
res.sendStatus(200)
} catch (err) {
logger.error(`[location.deleteTempImage] ${i18n.t('DB_ERROR')} ${image}`, err)
diff --git a/api/src/controllers/stripeController.ts b/api/src/controllers/stripeController.ts
index f9613c395..51910dac6 100644
--- a/api/src/controllers/stripeController.ts
+++ b/api/src/controllers/stripeController.ts
@@ -127,6 +127,11 @@ export const checkCheckoutSession = async (req: Request, res: Response) => {
// (Set BookingStatus to Paid and remove expireAt TTL index)
//
if (session.payment_status === 'paid') {
+ const supplier = await User.findById(booking.supplier)
+ if (!supplier) {
+ throw new Error(`Supplier ${booking.supplier} not found`)
+ }
+
booking.expireAt = undefined
booking.status = bookcarsTypes.BookingStatus.Paid
await booking.save()
@@ -135,32 +140,25 @@ export const checkCheckoutSession = async (req: Request, res: Response) => {
// await Car.updateOne({ _id: booking.car }, { available: false })
const car = await Car.findById(booking.car)
if (!car) {
- logger.info(`Car ${booking.car} not found`)
- return res.sendStatus(204)
+ throw new Error(`Car ${booking.car} not found`)
}
car.trips += 1
await car.save()
- // Send confirmation email
+ // Send confirmation email to customer
const user = await User.findById(booking.driver)
if (!user) {
- logger.info(`Driver ${booking.driver} not found`)
- return res.sendStatus(204)
+ throw new Error(`Driver ${booking.driver} not found`)
}
user.expireAt = undefined
await user.save()
- if (!await bookingController.confirm(user, booking, false)) {
+ if (!await bookingController.confirm(user, supplier, booking, false)) {
return res.sendStatus(400)
}
// Notify supplier
- const supplier = await User.findById(booking.supplier)
- if (!supplier) {
- logger.info(`Supplier ${booking.supplier} not found`)
- return res.sendStatus(204)
- }
i18n.locale = supplier.language
let message = i18n.t('BOOKING_PAID_NOTIFICATION')
await bookingController.notify(user, booking.id, supplier, message)
diff --git a/api/src/controllers/supplierController.ts b/api/src/controllers/supplierController.ts
index d446c21d9..7c3dd69b8 100644
--- a/api/src/controllers/supplierController.ts
+++ b/api/src/controllers/supplierController.ts
@@ -3,6 +3,7 @@ import fs from 'node:fs/promises'
import escapeStringRegexp from 'escape-string-regexp'
import { Request, Response } from 'express'
import mongoose from 'mongoose'
+import { nanoid } from 'nanoid'
import * as bookcarsTypes from ':bookcars-types'
import i18n from '../lang/i18n'
import * as env from '../config/env.config'
@@ -115,23 +116,34 @@ export const deleteSupplier = async (req: Request, res: Response) => {
if (await helper.exists(avatar)) {
await fs.unlink(avatar)
}
+ }
- await NotificationCounter.deleteMany({ user: id })
- await Notification.deleteMany({ user: id })
- const additionalDrivers = (await Booking.find({ supplier: id, _additionalDriver: { $ne: null } }, { _id: 0, _additionalDriver: 1 })).map((b) => b._additionalDriver)
- await AdditionalDriver.deleteMany({ _id: { $in: additionalDrivers } })
- await Booking.deleteMany({ supplier: id })
- const cars = await Car.find({ supplier: id })
- await Car.deleteMany({ supplier: id })
- for (const car of cars) {
- if (car.image) {
- const image = path.join(env.CDN_CARS, car.image)
- if (await helper.exists(image)) {
- await fs.unlink(image)
+ if (supplier.contracts) {
+ for (const contract of supplier.contracts) {
+ if (contract.file) {
+ const file = path.join(env.CDN_CONTRACTS, contract.file)
+ if (await helper.exists(file)) {
+ await fs.unlink(file)
}
}
}
}
+
+ await NotificationCounter.deleteMany({ user: id })
+ await Notification.deleteMany({ user: id })
+ const additionalDrivers = (await Booking.find({ supplier: id, _additionalDriver: { $ne: null } }, { _id: 0, _additionalDriver: 1 })).map((b) => b._additionalDriver)
+ await AdditionalDriver.deleteMany({ _id: { $in: additionalDrivers } })
+ await Booking.deleteMany({ supplier: id })
+ const cars = await Car.find({ supplier: id })
+ await Car.deleteMany({ supplier: id })
+ for (const car of cars) {
+ if (car.image) {
+ const image = path.join(env.CDN_CARS, car.image)
+ if (await helper.exists(image)) {
+ await fs.unlink(image)
+ }
+ }
+ }
} else {
return res.sendStatus(204)
}
@@ -530,3 +542,155 @@ export const getBackendSuppliers = async (req: Request, res: Response) => {
return res.status(400).send(i18n.t('DB_ERROR') + err)
}
}
+
+/**
+ * Upload a contract to temp folder.
+ *
+ * @export
+ * @async
+ * @param {Request} req
+ * @param {Response} res
+ * @returns {unknown}
+ */
+export const createContract = async (req: Request, res: Response) => {
+ const { language } = req.params
+
+ try {
+ if (!req.file) {
+ throw new Error('req.file not found')
+ }
+ if (language.length !== 2) {
+ throw new Error('Language not valid')
+ }
+
+ const filename = `${nanoid()}_${language}${path.extname(req.file.originalname)}`
+ const filepath = path.join(env.CDN_TEMP_CONTRACTS, filename)
+
+ await fs.writeFile(filepath, req.file.buffer)
+ return res.json(filename)
+ } catch (err) {
+ logger.error(`[supplier.createContract] ${i18n.t('DB_ERROR')}`, err)
+ return res.status(400).send(i18n.t('ERROR') + err)
+ }
+}
+
+/**
+ * Update a contract.
+ *
+ * @export
+ * @async
+ * @param {Request} req
+ * @param {Response} res
+ * @returns {unknown}
+ */
+export const updateContract = async (req: Request, res: Response) => {
+ const { id, language } = req.params
+ const { file } = req
+
+ try {
+ if (!file) {
+ throw new Error('req.file not found')
+ }
+ if (!helper.isValidObjectId(id)) {
+ throw new Error('Supplier Id not valid')
+ }
+ if (language.length !== 2) {
+ throw new Error('Language not valid')
+ }
+
+ const supplier = await User.findOne({ _id: id, type: bookcarsTypes.UserType.Supplier })
+
+ if (supplier) {
+ const contract = supplier.contracts?.find((c) => c.language === language)
+ if (contract?.file) {
+ const contractFile = path.join(env.CDN_CONTRACTS, contract.file)
+ if (await helper.exists(contractFile)) {
+ await fs.unlink(contractFile)
+ }
+ }
+
+ const filename = `${supplier._id}_${language}${path.extname(file.originalname)}`
+ const filepath = path.join(env.CDN_CONTRACTS, filename)
+
+ await fs.writeFile(filepath, file.buffer)
+ if (!contract) {
+ supplier.contracts?.push({ language, file: filename })
+ } else {
+ contract.file = filename
+ }
+ await supplier.save()
+ return res.json(filename)
+ }
+
+ return res.sendStatus(204)
+ } catch (err) {
+ logger.error(`[supplier.updateContract] ${i18n.t('DB_ERROR')} ${id}`, err)
+ return res.status(400).send(i18n.t('DB_ERROR') + err)
+ }
+}
+
+/**
+ * Delete a contract.
+ *
+ * @export
+ * @async
+ * @param {Request} req
+ * @param {Response} res
+ * @returns {unknown}
+ */
+export const deleteContract = async (req: Request, res: Response) => {
+ const { id, language } = req.params
+
+ try {
+ if (!helper.isValidObjectId(id)) {
+ throw new Error('Supplier Id not valid')
+ }
+ if (language.length !== 2) {
+ throw new Error('Language not valid')
+ }
+ const supplier = await User.findOne({ _id: id, type: bookcarsTypes.UserType.Supplier })
+
+ if (supplier) {
+ const contract = supplier.contracts?.find((c) => c.language === language)
+ if (contract?.file) {
+ const contractFile = path.join(env.CDN_CONTRACTS, contract.file)
+ if (await helper.exists(contractFile)) {
+ await fs.unlink(contractFile)
+ }
+ contract.file = null
+ }
+
+ await supplier.save()
+ return res.sendStatus(200)
+ }
+ return res.sendStatus(204)
+ } catch (err) {
+ logger.error(`[supplier.deleteContract] ${i18n.t('DB_ERROR')} ${id}`, err)
+ return res.status(400).send(i18n.t('DB_ERROR') + err)
+ }
+}
+
+/**
+ * Delete a temp contract.
+ *
+ * @export
+ * @async
+ * @param {Request} req
+ * @param {Response} res
+ * @returns {*}
+ */
+export const deleteTempContract = async (req: Request, res: Response) => {
+ const { file } = req.params
+
+ try {
+ const contractFile = path.join(env.CDN_TEMP_CONTRACTS, file)
+ if (await helper.exists(contractFile)) {
+ await fs.unlink(contractFile)
+ }
+
+ res.sendStatus(200)
+ } catch (err) {
+ logger.error(`[supplier.deleteTempContract] ${i18n.t('DB_ERROR')} ${file}`, err)
+ res.status(400).send(i18n.t('ERROR') + err)
+ }
+}
diff --git a/api/src/controllers/userController.ts b/api/src/controllers/userController.ts
index f1abab242..c1e7e8a3c 100644
--- a/api/src/controllers/userController.ts
+++ b/api/src/controllers/userController.ts
@@ -168,9 +168,31 @@ export const create = async (req: Request, res: Response) => {
body.password = passwordHash
}
+ const { contracts } = body
+ body.contracts = undefined
+
const user = new User(body)
await user.save()
+ const finalContracts: bookcarsTypes.Contract[] = []
+ if (contracts) {
+ for (const contract of contracts) {
+ if (contract.language && contract.file) {
+ const tempFile = path.join(env.CDN_TEMP_CONTRACTS, contract.file)
+
+ if (await helper.exists(tempFile)) {
+ const filename = `${user.id}_${contract.language}${path.extname(tempFile)}`
+ const newPath = path.join(env.CDN_CONTRACTS, filename)
+
+ await fs.rename(tempFile, newPath)
+ finalContracts.push({ language: contract.language, file: filename })
+ }
+ }
+ }
+ user.contracts = finalContracts
+ await user.save()
+ }
+
// avatar
if (body.avatar) {
const avatar = path.join(env.CDN_TEMP_USERS, body.avatar)
@@ -1412,6 +1434,17 @@ export const deleteUsers = async (req: Request, res: Response) => {
}
}
+ if (user.contracts) {
+ for (const contract of user.contracts) {
+ if (contract.file) {
+ const file = path.join(env.CDN_CONTRACTS, contract.file)
+ if (await helper.exists(file)) {
+ await fs.unlink(file)
+ }
+ }
+ }
+ }
+
if (user.type === bookcarsTypes.UserType.Supplier) {
const additionalDrivers = (await Booking.find({ supplier: id, _additionalDriver: { $ne: null } }, { _id: 0, _additionalDriver: 1 })).map((b) => b._additionalDriver)
await AdditionalDriver.deleteMany({ _id: { $in: additionalDrivers } })
diff --git a/api/src/models/User.ts b/api/src/models/User.ts
index 6c3dda816..b96dea0ec 100644
--- a/api/src/models/User.ts
+++ b/api/src/models/User.ts
@@ -103,6 +103,17 @@ const userSchema = new Schema(
customerId: {
type: String,
},
+ contracts: [{
+ language: {
+ type: String,
+ required: [true, "can't be blank"],
+ trim: true,
+ lowercase: true,
+ minLength: 2,
+ maxLength: 2,
+ },
+ file: String,
+ }],
expireAt: {
//
// Non verified and active users created from checkout with Stripe are temporary and
diff --git a/api/src/routes/supplierRoutes.ts b/api/src/routes/supplierRoutes.ts
index bec8f11c7..13453b47c 100644
--- a/api/src/routes/supplierRoutes.ts
+++ b/api/src/routes/supplierRoutes.ts
@@ -1,4 +1,5 @@
import express from 'express'
+import multer from 'multer'
import routeNames from '../config/supplierRoutes.config'
import authJwt from '../middlewares/authJwt'
import * as supplierController from '../controllers/supplierController'
@@ -13,5 +14,9 @@ routes.route(routeNames.getSuppliers).get(authJwt.verifyToken, supplierControlle
routes.route(routeNames.getAllSuppliers).get(supplierController.getAllSuppliers)
routes.route(routeNames.getFrontendSuppliers).post(supplierController.getFrontendSuppliers)
routes.route(routeNames.getBackendSuppliers).post(authJwt.verifyToken, supplierController.getBackendSuppliers)
+routes.route(routeNames.createContract).post([authJwt.verifyToken, multer({ storage: multer.memoryStorage() }).single('file')], supplierController.createContract)
+routes.route(routeNames.updateContract).post([authJwt.verifyToken, multer({ storage: multer.memoryStorage() }).single('file')], supplierController.updateContract)
+routes.route(routeNames.deleteContract).post(authJwt.verifyToken, supplierController.deleteContract)
+routes.route(routeNames.deleteTempContract).post(authJwt.verifyToken, supplierController.deleteTempContract)
export default routes
diff --git a/api/tests/booking.test.ts b/api/tests/booking.test.ts
index 1f8e671e5..e941e38fe 100644
--- a/api/tests/booking.test.ts
+++ b/api/tests/booking.test.ts
@@ -1,11 +1,15 @@
import 'dotenv/config'
import request from 'supertest'
+import url from 'url'
+import path from 'path'
+import fs from 'node:fs/promises'
import { nanoid } from 'nanoid'
import mongoose from 'mongoose'
import * as bookcarsTypes from ':bookcars-types'
import app from '../src/app'
import * as databaseHelper from '../src/common/databaseHelper'
import * as testHelper from './testHelper'
+import * as helper from '../src/common/helper'
import Car from '../src/models/Car'
import Booking from '../src/models/Booking'
import AdditionalDriver from '../src/models/AdditionalDriver'
@@ -15,6 +19,12 @@ import PushToken from '../src/models/PushToken'
import Token from '../src/models/Token'
import stripeAPI from '../src/stripe'
+const __filename = url.fileURLToPath(import.meta.url)
+const __dirname = path.dirname(__filename)
+
+const CONTRACT1 = 'contract1.pdf'
+const CONTRACT1_PATH = path.join(__dirname, `./contracts/${CONTRACT1}`)
+
const DRIVER1_NAME = 'Driver 1'
let SUPPLIER_ID: string
@@ -49,6 +59,15 @@ beforeAll(async () => {
const supplierName = testHelper.getSupplierName()
SUPPLIER_ID = await testHelper.createSupplier(`${supplierName}@test.bookcars.ma`, supplierName)
+ const contractFileName = `${SUPPLIER_ID}_en.pdf`
+ const contractFile = path.join(env.CDN_CONTRACTS, contractFileName)
+ if (!await helper.exists(contractFile)) {
+ await fs.copyFile(CONTRACT1_PATH, contractFile)
+ }
+ const supplier = await User.findById(SUPPLIER_ID)
+ supplier!.contracts = [{ language: 'en', file: contractFileName }]
+ await supplier?.save()
+
// create driver 1
const driver1 = new User({
fullName: DRIVER1_NAME,
@@ -351,14 +370,14 @@ describe('POST /api/checkout', () => {
res = await request(app)
.post('/api/checkout')
.send(payload)
- expect(res.statusCode).toBe(204)
+ expect(res.statusCode).toBe(400)
payload.booking!.supplier = SUPPLIER_ID
payload.booking!.driver = testHelper.GetRandromObjectIdAsString()
res = await request(app)
.post('/api/checkout')
.send(payload)
- expect(res.statusCode).toBe(204)
+ expect(res.statusCode).toBe(400)
payload.booking = undefined
res = await request(app)
diff --git a/api/tests/car.test.ts b/api/tests/car.test.ts
index 1732fc88e..1854ca7c5 100644
--- a/api/tests/car.test.ts
+++ b/api/tests/car.test.ts
@@ -18,9 +18,9 @@ const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const IMAGE1 = 'bmw-x1.jpg'
-const IMAGE1_PATH = path.resolve(__dirname, `./img/${IMAGE1}`)
+const IMAGE1_PATH = path.join(__dirname, `./img/${IMAGE1}`)
const IMAGE2 = 'bmw-x5.jpg'
-const IMAGE2_PATH = path.resolve(__dirname, `./img/${IMAGE2}`)
+const IMAGE2_PATH = path.join(__dirname, `./img/${IMAGE2}`)
let SUPPLIER1_ID: string
let SUPPLIER2_ID: string
@@ -319,7 +319,7 @@ describe('POST /api/create-car-image', () => {
.attach('image', IMAGE1_PATH)
expect(res.statusCode).toBe(200)
const filename = res.body as string
- const filePath = path.resolve(env.CDN_TEMP_CARS, filename)
+ const filePath = path.join(env.CDN_TEMP_CARS, filename)
const imageExists = await helper.exists(filePath)
expect(imageExists).toBeTruthy()
await fs.unlink(filePath)
@@ -343,7 +343,7 @@ describe('POST /api/update-car-image/:id', () => {
.attach('image', IMAGE2_PATH)
expect(res.statusCode).toBe(200)
const filename = res.body as string
- const imageExists = await helper.exists(path.resolve(env.CDN_CARS, filename))
+ const imageExists = await helper.exists(path.join(env.CDN_CARS, filename))
expect(imageExists).toBeTruthy()
const car = await Car.findById(CAR_ID)
expect(car).not.toBeNull()
diff --git a/api/tests/contracts/contract1.pdf b/api/tests/contracts/contract1.pdf
new file mode 100644
index 000000000..06a366a35
Binary files /dev/null and b/api/tests/contracts/contract1.pdf differ
diff --git a/api/tests/contracts/contract2.pdf b/api/tests/contracts/contract2.pdf
new file mode 100644
index 000000000..06a366a35
Binary files /dev/null and b/api/tests/contracts/contract2.pdf differ
diff --git a/api/tests/location.test.ts b/api/tests/location.test.ts
index a767065fa..919f48f18 100644
--- a/api/tests/location.test.ts
+++ b/api/tests/location.test.ts
@@ -20,11 +20,11 @@ const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const IMAGE0 = 'location0.jpg'
-const IMAGE0_PATH = path.resolve(__dirname, `./img/${IMAGE0}`)
+const IMAGE0_PATH = path.join(__dirname, `./img/${IMAGE0}`)
const IMAGE1 = 'location1.jpg'
-const IMAGE1_PATH = path.resolve(__dirname, `./img/${IMAGE1}`)
+const IMAGE1_PATH = path.join(__dirname, `./img/${IMAGE1}`)
const IMAGE2 = 'location2.jpg'
-const IMAGE2_PATH = path.resolve(__dirname, `./img/${IMAGE2}`)
+const IMAGE2_PATH = path.join(__dirname, `./img/${IMAGE2}`)
let LOCATION_ID: string
@@ -320,7 +320,7 @@ describe('POST /api/create-location-image', () => {
.attach('image', IMAGE1_PATH)
expect(res.statusCode).toBe(200)
const filename = res.body as string
- const filePath = path.resolve(env.CDN_TEMP_LOCATIONS, filename)
+ const filePath = path.join(env.CDN_TEMP_LOCATIONS, filename)
const imageExists = await helper.exists(filePath)
expect(imageExists).toBeTruthy()
await fs.unlink(filePath)
@@ -344,7 +344,7 @@ describe('POST /api/update-location-image/:id', () => {
.attach('image', IMAGE2_PATH)
expect(res.statusCode).toBe(200)
const filename = res.body as string
- const imageExists = await helper.exists(path.resolve(env.CDN_LOCATIONS, filename))
+ const imageExists = await helper.exists(path.join(env.CDN_LOCATIONS, filename))
expect(imageExists).toBeTruthy()
const location = await Location.findById(LOCATION_ID)
expect(location).not.toBeNull()
@@ -395,14 +395,14 @@ describe('POST /api/delete-location-image/:id', () => {
expect(location).not.toBeNull()
expect(location?.image).toBeDefined()
const filename = location?.image as string
- let imageExists = await helper.exists(path.resolve(env.CDN_LOCATIONS, filename))
+ let imageExists = await helper.exists(path.join(env.CDN_LOCATIONS, filename))
expect(imageExists).toBeTruthy()
let res = await request(app)
.post(`/api/delete-location-image/${LOCATION_ID}`)
.set(env.X_ACCESS_TOKEN, token)
expect(res.statusCode).toBe(200)
- imageExists = await helper.exists(path.resolve(env.CDN_LOCATIONS, filename))
+ imageExists = await helper.exists(path.join(env.CDN_LOCATIONS, filename))
expect(imageExists).toBeFalsy()
location = await Location.findById(LOCATION_ID)
expect(location?.image).toBeNull()
@@ -439,7 +439,7 @@ describe('POST /api/delete-temp-location-image/:image', () => {
res = await request(app)
.post('/api/delete-temp-location-image/unknown.jpg')
.set(env.X_ACCESS_TOKEN, token)
- expect(res.statusCode).toBe(400)
+ expect(res.statusCode).toBe(200)
await testHelper.signout(token)
})
diff --git a/api/tests/supplier.test.ts b/api/tests/supplier.test.ts
index 666f679c5..f57085314 100644
--- a/api/tests/supplier.test.ts
+++ b/api/tests/supplier.test.ts
@@ -19,6 +19,11 @@ import AdditionalDriver from '../src/models/AdditionalDriver'
const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
+const CONTRACT1 = 'contract1.pdf'
+const CONTRACT1_PATH = path.join(__dirname, `./contracts/${CONTRACT1}`)
+const CONTRACT2 = 'contract2.pdf'
+const CONTRACT2_PATH = path.join(__dirname, `./contracts/${CONTRACT2}`)
+
const LOCATION_ID = testHelper.GetRandromObjectIdAsString()
let SUPPLIER1_ID: string
@@ -273,16 +278,24 @@ describe('DELETE /api/delete-supplier/:id', () => {
let supplier = await User.findById(supplierId)
expect(supplier).not.toBeNull()
let avatarName = 'avatar1.jpg'
- let avatarPath = path.resolve(__dirname, `./img/${avatarName}`)
+ let avatarPath = path.join(__dirname, `./img/${avatarName}`)
let avatar = path.join(env.CDN_USERS, avatarName)
if (!await helper.exists(avatar)) {
await fs.copyFile(avatarPath, avatar)
}
supplier!.avatar = avatarName
+
+ const contractFileName = `${nanoid()}.pdf`
+ const contractFile = path.join(env.CDN_CONTRACTS, contractFileName)
+ if (!await helper.exists(contractFile)) {
+ await fs.copyFile(CONTRACT1_PATH, contractFile)
+ }
+ supplier!.contracts = [{ language: 'en', file: contractFileName }]
+
await supplier?.save()
let locationId = await testHelper.createLocation('Location 1 EN', 'Location 1 FR')
const carImageName = 'bmw-x1.jpg'
- const carImagePath = path.resolve(__dirname, `./img/${carImageName}`)
+ const carImagePath = path.join(__dirname, `./img/${carImageName}`)
let car = new Car({
name: 'BMW X1',
supplier: supplierId,
@@ -344,6 +357,8 @@ describe('DELETE /api/delete-supplier/:id', () => {
expect(res.statusCode).toBe(200)
supplier = await User.findById(supplierId)
expect(supplier).toBeNull()
+ expect(await helper.exists(avatar)).toBeFalsy()
+ expect(await helper.exists(contractFile)).toBeFalsy()
await testHelper.deleteLocation(locationId)
res = await request(app)
@@ -413,7 +428,7 @@ describe('DELETE /api/delete-supplier/:id', () => {
supplier = await User.findById(supplierId)
expect(supplier).not.toBeNull()
avatarName = 'avatar1.jpg'
- avatarPath = path.resolve(__dirname, `./img/${avatarName}`)
+ avatarPath = path.join(__dirname, `./img/${avatarName}`)
avatar = path.join(env.CDN_USERS, avatarName)
if (!await helper.exists(avatar)) {
await fs.copyFile(avatarPath, avatar)
@@ -635,3 +650,184 @@ describe('POST /api/backend-suppliers', () => {
await testHelper.signout(token)
})
})
+
+describe('POST /api/create-contract', () => {
+ it('should create a contract', async () => {
+ const token = await testHelper.signinAsAdmin()
+
+ // test success
+ let res = await request(app)
+ .post('/api/create-contract/en')
+ .set(env.X_ACCESS_TOKEN, token)
+ .attach('file', CONTRACT1_PATH)
+ expect(res.statusCode).toBe(200)
+ const filename = res.body as string
+ const filePath = path.join(env.CDN_TEMP_CONTRACTS, filename)
+ const imageExists = await helper.exists(filePath)
+ expect(imageExists).toBeTruthy()
+ await fs.unlink(filePath)
+
+ // test failure (file not sent)
+ res = await request(app)
+ .post('/api/create-contract/en')
+ .set(env.X_ACCESS_TOKEN, token)
+ expect(res.statusCode).toBe(400)
+
+ // test failure (language not valid)
+ res = await request(app)
+ .post('/api/create-contract/english')
+ .set(env.X_ACCESS_TOKEN, token)
+ .attach('file', CONTRACT1_PATH)
+ expect(res.statusCode).toBe(400)
+
+ await testHelper.signout(token)
+ })
+})
+
+describe('POST /api/update-contract/:id', () => {
+ it('should update a contract', async () => {
+ const token = await testHelper.signinAsAdmin()
+
+ // test success (no initial contract)
+ let supplier = await User.findById(SUPPLIER1_ID)
+ let res = await request(app)
+ .post(`/api/update-contract/${SUPPLIER1_ID}/en`)
+ .set(env.X_ACCESS_TOKEN, token)
+ .attach('file', CONTRACT1_PATH)
+ expect(res.statusCode).toBe(200)
+ let filename = res.body as string
+ expect(filename).toBeTruthy()
+ expect(await helper.exists(path.join(env.CDN_CONTRACTS, filename))).toBeTruthy()
+ supplier = await User.findById(SUPPLIER1_ID)
+ expect(supplier).toBeTruthy()
+ expect(supplier?.contracts?.find((c) => c.language === 'en')?.file).toBe(filename)
+
+ // test success (initial contract)
+ res = await request(app)
+ .post(`/api/update-contract/${SUPPLIER1_ID}/en`)
+ .set(env.X_ACCESS_TOKEN, token)
+ .attach('file', CONTRACT2_PATH)
+ expect(res.statusCode).toBe(200)
+ filename = res.body as string
+ expect(filename).toBeTruthy()
+ expect(await helper.exists(path.join(env.CDN_CONTRACTS, filename))).toBeTruthy()
+ supplier = await User.findById(SUPPLIER1_ID)
+ expect(filename).toBe(supplier?.contracts?.find((c) => c.language === 'en')?.file)
+
+ // test success (contract file does not exist)
+ supplier!.contracts!.find((c) => c.language === 'en')!.file = `${nanoid()}.pdf`
+ await supplier?.save()
+ res = await request(app)
+ .post(`/api/update-contract/${SUPPLIER1_ID}/en`)
+ .set(env.X_ACCESS_TOKEN, token)
+ .attach('file', CONTRACT1_PATH)
+ expect(res.statusCode).toBe(200)
+ filename = res.body as string
+ expect(filename).toBeTruthy()
+ expect(await helper.exists(path.join(env.CDN_CONTRACTS, filename))).toBeTruthy()
+ supplier = await User.findById(SUPPLIER1_ID)
+ expect(filename).toBe(supplier?.contracts?.find((c) => c.language === 'en')?.file)
+ supplier!.contracts!.find((c) => c.language === 'en')!.file = filename
+ await supplier?.save()
+
+ // test failure (file not sent)
+ res = await request(app)
+ .post(`/api/update-contract/${SUPPLIER1_ID}/en`)
+ .set(env.X_ACCESS_TOKEN, token)
+ expect(res.statusCode).toBe(400)
+
+ // test failure (supplier not found)
+ res = await request(app)
+ .post(`/api/update-contract/${testHelper.GetRandromObjectIdAsString()}/en`)
+ .set(env.X_ACCESS_TOKEN, token)
+ .attach('file', CONTRACT1_PATH)
+ expect(res.statusCode).toBe(204)
+
+ // test failure (supplier id not valid)
+ res = await request(app)
+ .post('/api/update-contract/0/en')
+ .set(env.X_ACCESS_TOKEN, token)
+ .attach('file', CONTRACT1_PATH)
+ expect(res.statusCode).toBe(400)
+
+ // test failure (language not valid)
+ res = await request(app)
+ .post(`/api/update-contract/${testHelper.GetRandromObjectIdAsString()}/english`)
+ .set(env.X_ACCESS_TOKEN, token)
+ .attach('file', CONTRACT1_PATH)
+ expect(res.statusCode).toBe(400)
+
+ await testHelper.signout(token)
+ })
+})
+
+describe('POST /api/delete-contract/:id', () => {
+ it('should delete a contract', async () => {
+ const token = await testHelper.signinAsAdmin()
+
+ let supplier = await User.findById(SUPPLIER1_ID)
+ expect(supplier).toBeTruthy()
+ expect(supplier?.contracts?.find((c) => c.language === 'en')?.file).toBeTruthy()
+ const filename = supplier?.contracts?.find((c) => c.language === 'en')?.file as string
+ let imageExists = await helper.exists(path.join(env.CDN_CONTRACTS, filename))
+ expect(imageExists).toBeTruthy()
+
+ // test success
+ let res = await request(app)
+ .post(`/api/delete-contract/${SUPPLIER1_ID}/en`)
+ .set(env.X_ACCESS_TOKEN, token)
+ expect(res.statusCode).toBe(200)
+ imageExists = await helper.exists(path.join(env.CDN_CONTRACTS, filename))
+ expect(imageExists).toBeFalsy()
+ supplier = await User.findById(SUPPLIER1_ID)
+ expect(supplier?.contracts?.find((c) => c.language === 'en')?.file).toBeFalsy()
+
+ // test failure (supplier not found)
+ res = await request(app)
+ .post(`/api/delete-contract/${testHelper.GetRandromObjectIdAsString()}/en`)
+ .set(env.X_ACCESS_TOKEN, token)
+ expect(res.statusCode).toBe(204)
+
+ // test failure (supplier id not valid)
+ res = await request(app)
+ .post('/api/delete-contract/invalid-id/en')
+ .set(env.X_ACCESS_TOKEN, token)
+ expect(res.statusCode).toBe(400)
+
+ // test failure (language id not valid)
+ res = await request(app)
+ .post(`/api/delete-contract/${testHelper.GetRandromObjectIdAsString()}/english`)
+ .set(env.X_ACCESS_TOKEN, token)
+ expect(res.statusCode).toBe(400)
+
+ await testHelper.signout(token)
+ })
+})
+
+describe('POST /api/delete-temp-contract/:image', () => {
+ it('should delete a temporary contract', async () => {
+ const token = await testHelper.signinAsAdmin()
+
+ // init
+ const tempImage = path.join(env.CDN_TEMP_CONTRACTS, CONTRACT1)
+ if (!await helper.exists(tempImage)) {
+ await fs.copyFile(CONTRACT1_PATH, tempImage)
+ }
+
+ // test success (temp file exists)
+ let res = await request(app)
+ .post(`/api/delete-temp-contract/${CONTRACT1}`)
+ .set(env.X_ACCESS_TOKEN, token)
+ expect(res.statusCode).toBe(200)
+ const tempImageExists = await helper.exists(tempImage)
+ expect(tempImageExists).toBeFalsy()
+
+ // test success (temp file not found)
+ res = await request(app)
+ .post('/api/delete-temp-contract/unknown.pdf')
+ .set(env.X_ACCESS_TOKEN, token)
+ expect(res.statusCode).toBe(200)
+
+ await testHelper.signout(token)
+ })
+})
diff --git a/api/tests/user.test.ts b/api/tests/user.test.ts
index 0279bd72e..be8f85401 100644
--- a/api/tests/user.test.ts
+++ b/api/tests/user.test.ts
@@ -9,6 +9,7 @@ import * as bookcarsTypes from ':bookcars-types'
import app from '../src/app'
import * as databaseHelper from '../src/common/databaseHelper'
import * as testHelper from './testHelper'
+import * as helper from '../src/common/helper'
import * as env from '../src/config/env.config'
import User from '../src/models/User'
import Token from '../src/models/Token'
@@ -16,15 +17,17 @@ import PushToken from '../src/models/PushToken'
import Car from '../src/models/Car'
import Booking from '../src/models/Booking'
import AdditionalDriver from '../src/models/AdditionalDriver'
-import * as helper from '../src/common/helper'
const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const AVATAR1 = 'avatar1.jpg'
-const AVATAR1_PATH = path.resolve(__dirname, `./img/${AVATAR1}`)
+const AVATAR1_PATH = path.join(__dirname, `./img/${AVATAR1}`)
const AVATAR2 = 'avatar2.png'
-const AVATAR2_PATH = path.resolve(__dirname, `./img/${AVATAR2}`)
+const AVATAR2_PATH = path.join(__dirname, `./img/${AVATAR2}`)
+
+const CONTRACT1 = 'contract1.pdf'
+const CONTRACT1_PATH = path.join(__dirname, `./contracts/${CONTRACT1}`)
let USER1_ID: string
let USER2_ID: string
@@ -111,6 +114,12 @@ describe('POST /api/sign-up', () => {
expect(token?.token.length).toBeGreaterThan(0)
await token?.deleteOne()
+ payload.email = 'wrong-email'
+ res = await request(app)
+ .post('/api/sign-up')
+ .send(payload)
+ expect(res.statusCode).toBe(400)
+
res = await request(app)
.post('/api/sign-up')
expect(res.statusCode).toBe(400)
@@ -157,6 +166,14 @@ describe('POST /api/create-user', () => {
if (!await helper.exists(tempAvatar)) {
await fs.copyFile(AVATAR1_PATH, tempAvatar)
}
+
+ const contractFileName = `${nanoid()}.pdf`
+ const contractFile = path.join(env.CDN_TEMP_CONTRACTS, contractFileName)
+ if (!await helper.exists(contractFile)) {
+ await fs.copyFile(CONTRACT1_PATH, contractFile)
+ }
+ const contracts = [{ language: 'en', file: contractFileName }]
+
let payload: bookcarsTypes.CreateUserPayload = {
email: USER2_EMAIL,
fullName: 'user2',
@@ -166,6 +183,7 @@ describe('POST /api/create-user', () => {
location: 'location',
bio: 'bio',
avatar: AVATAR1,
+ contracts,
}
let res = await request(app)
.post('/api/create-user')
@@ -183,6 +201,9 @@ describe('POST /api/create-user', () => {
expect(user?.phone).toBe(payload.phone)
expect(user?.location).toBe(payload.location)
expect(user?.bio).toBe(payload.bio)
+ const contractFileResult = user?.contracts?.find((c) => c.language === 'en')?.file
+ expect(contractFileResult).toBeTruthy()
+ expect(await helper.exists(path.join(env.CDN_CONTRACTS, contractFileResult!))).toBeTruthy()
let userToken = await Token.findOne({ user: USER2_ID })
expect(userToken).not.toBeNull()
expect(userToken?.token.length).toBeGreaterThan(0)
@@ -948,7 +969,7 @@ describe('POST /api/create-avatar', () => {
.attach('image', AVATAR1_PATH)
expect(res.statusCode).toBe(200)
const filename = res.body as string
- const filePath = path.resolve(env.CDN_TEMP_USERS, filename)
+ const filePath = path.join(env.CDN_TEMP_USERS, filename)
const avatarExists = await helper.exists(filePath)
expect(avatarExists).toBeTruthy()
await fs.unlink(filePath)
@@ -972,7 +993,7 @@ describe('POST /api/update-avatar/:userId', () => {
.attach('image', AVATAR2_PATH)
expect(res.statusCode).toBe(200)
const filename = res.body as string
- let avatarExists = await helper.exists(path.resolve(env.CDN_USERS, filename))
+ let avatarExists = await helper.exists(path.join(env.CDN_USERS, filename))
expect(avatarExists).toBeTruthy()
const user = await User.findById(USER1_ID)
expect(user).not.toBeNull()
@@ -986,7 +1007,7 @@ describe('POST /api/update-avatar/:userId', () => {
.set(env.X_ACCESS_TOKEN, token)
.attach('image', AVATAR2_PATH)
expect(res.statusCode).toBe(200)
- avatarExists = await helper.exists(path.resolve(env.CDN_USERS, filename))
+ avatarExists = await helper.exists(path.join(env.CDN_USERS, filename))
expect(avatarExists).toBeTruthy()
user!.avatar = undefined
@@ -996,7 +1017,7 @@ describe('POST /api/update-avatar/:userId', () => {
.set(env.X_ACCESS_TOKEN, token)
.attach('image', AVATAR2_PATH)
expect(res.statusCode).toBe(200)
- avatarExists = await helper.exists(path.resolve(env.CDN_USERS, filename))
+ avatarExists = await helper.exists(path.join(env.CDN_USERS, filename))
expect(avatarExists).toBeTruthy()
res = await request(app)
@@ -1290,7 +1311,7 @@ describe('POST /api/delete-users', () => {
const supplierId = await testHelper.createSupplier(`${supplierName}@test.bookcars.ma`, supplierName)
const locationId = await testHelper.createLocation('Location 1 EN', 'Location 1 FR')
const imageName = 'bmw-x1.jpg'
- const imagePath = path.resolve(__dirname, `./img/${imageName}`)
+ const imagePath = path.join(__dirname, `./img/${imageName}`)
const image = path.join(env.CDN_CARS, imageName)
if (!await helper.exists(image)) {
await fs.copyFile(imagePath, image)
diff --git a/backend/.env.docker.example b/backend/.env.docker.example
index 1743008ef..d354d3d54 100644
--- a/backend/.env.docker.example
+++ b/backend/.env.docker.example
@@ -10,6 +10,8 @@ VITE_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
VITE_BC_CDN_TEMP_CARS=http://localhost/cdn/bookcars/temp/cars
VITE_BC_CDN_LOCATIONS=http://localhost/cdn/bookcars/locations
VITE_BC_CDN_TEMP_LOCATIONS=http://localhost/cdn/bookcars/temp/locations
+VITE_BC_CDN_CONTRACTS=http://localhost/cdn/bookcars/contracts
+VITE_BC_CDN_TEMP_CONTRACTS=http://localhost/cdn/bookcars/temp/contracts
VITE_BC_SUPPLIER_IMAGE_WIDTH=60
VITE_BC_SUPPLIER_IMAGE_HEIGHT=30
VITE_BC_CAR_IMAGE_WIDTH=300
diff --git a/backend/.env.example b/backend/.env.example
index 3af6727e3..d4e23b5b6 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -11,6 +11,8 @@ VITE_BC_CDN_CARS=http://localhost/cdn/bookcars/cars
VITE_BC_CDN_TEMP_CARS=http://localhost/cdn/bookcars/temp/cars
VITE_BC_CDN_LOCATIONS=http://localhost/cdn/bookcars/locations
VITE_BC_CDN_TEMP_LOCATIONS=http://localhost/cdn/bookcars/temp/locations
+VITE_BC_CDN_CONTRACTS=http://localhost/cdn/bookcars/contracts
+VITE_BC_CDN_TEMP_CONTRACTS=http://localhost/cdn/bookcars/temp/contracts
VITE_BC_SUPPLIER_IMAGE_WIDTH=60
VITE_BC_SUPPLIER_IMAGE_HEIGHT=30
VITE_BC_CAR_IMAGE_WIDTH=300
diff --git a/packages/bookcars-types/index.ts b/packages/bookcars-types/index.ts
index 855383b71..747d03e9e 100644
--- a/packages/bookcars-types/index.ts
+++ b/packages/bookcars-types/index.ts
@@ -238,6 +238,8 @@ export interface SignUpPayload {
birthDate?: number | Date
}
+export type Contract = { language: string, file: string | null }
+
export interface CreateUserPayload {
email?: string
phone: string
@@ -253,6 +255,7 @@ export interface CreateUserPayload {
blacklisted?: boolean
payLater?: boolean
supplier?: string
+ contracts?: Contract[]
}
export interface UpdateUserPayload extends CreateUserPayload {