From 82c2ea74e823350413c6efc647d64742a194bbde Mon Sep 17 00:00:00 2001 From: Josepablo Cruz Baas Date: Tue, 31 Mar 2026 02:02:30 +0000 Subject: [PATCH 1/3] feat: Private Groups + Load-Templates Private Groups enabled based on the following assumptions: * There is only one private group. * The companies added to the private group is able to find private loads in the directory. * The companies added to the private group is able to find private companies in the directory. * The driver requires a new endpoint to read load resources no matter the privacy rules (as long as they are added to the load as driver). * The private group can be updated by anyone within the company. * The privacy is enabled on the company information by enabling `privacy=true`. * The privacy is enabled on the loads information by enabling `privacy=true`. * When looking for loads/companies by default the search is with `privacy=false` which returns public elements. When `privacy=true`is given, the results are limited to private elements only (not including public elements). Load-Templates enabled based on the following assumptions: * Anyone can CRUD the templates, there is no find feature (for now). It is assumed that the number of templates is limited. --- .gitignore | 3 +- v1/README.md | 144 ++++++++++++++++-- v1/src/apps/private/companies/services.js | 80 +++++++--- v1/src/apps/private/groups/routes.js | 9 ++ v1/src/apps/private/groups/services.js | 116 ++++++++++++++ v1/src/apps/private/index.js | 17 +-- v1/src/apps/private/load-templates/routes.js | 11 ++ .../apps/private/load-templates/services.js | 137 +++++++++++++++++ v1/src/apps/private/loads/routes.js | 1 + v1/src/apps/private/loads/services.js | 93 ++++++++++- .../apps/public/public-companies/services.js | 12 +- v1/src/apps/public/public-loads/services.js | 11 +- .../Handlers/MailClient/StandAlone.handler.js | 2 +- v1/src/lib/Models/companies.model.js | 1 + v1/src/lib/Models/company_groups.models.js | 12 ++ v1/src/lib/Models/index.js | 6 + v1/src/lib/Models/load_templates.model.js | 68 +++++++++ v1/src/lib/Models/loads.model.js | 4 +- 18 files changed, 672 insertions(+), 55 deletions(-) create mode 100644 v1/src/apps/private/groups/routes.js create mode 100644 v1/src/apps/private/groups/services.js create mode 100644 v1/src/apps/private/load-templates/routes.js create mode 100644 v1/src/apps/private/load-templates/services.js create mode 100644 v1/src/lib/Models/company_groups.models.js create mode 100644 v1/src/lib/Models/load_templates.model.js diff --git a/.gitignore b/.gitignore index ffc83f1..20bd1d0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ **/migrate.js **/scripts/migrate/* v1/src/config/*.json -**/apiConfig.json \ No newline at end of file +**/apiConfig.json +ignore/* diff --git a/v1/README.md b/v1/README.md index 2e1b1cb..6576e3e 100644 --- a/v1/README.md +++ b/v1/README.md @@ -7,6 +7,11 @@ ETA Viaporte API - NodeJS v18 - Docker +# Audience + +This document is oriented to front-end developers integrating with the ETA API. +Use the Quick Start section first, then go to the endpoint section you need. + # Endpoints All endpoints that return a list of elements is paginable with the following queries: @@ -18,6 +23,71 @@ Example: - `/endpoint?page=2` : Get page 2 with default (10) elements per page. - `/endpoint?elements=50` : Get page 0 with 50 elements. +## Quick Start For Front-End Integration + +### Authentication + +Private endpoints require JWT authorization. + +1. Login with `POST /account/authorize`. +2. Store `accessToken` and `session_token`. +3. Send JWT in request headers: + +```http +Authorization: Bearer +``` + +4. When JWT expires, request a new token with `GET /account/authorize/:session_token`. + +### Account Lifecycle (Common Front-End Flow) + +1. Signup request: `POST /account/signup` returns `checksum`. +2. Signup confirmation: `PATCH /account/signup` with `otp` + `checksum`. +3. Login: `POST /account/authorize`. +4. Register company profile: `POST /account/register`. + +### Pagination + +Pagination is available on list endpoints using: + + - `elements`: page size + - `page`: page index (starts at 0) + +Examples: + + - `/loads?elements=20&page=0` + - `/loads?page=1` + - `/loads?elements=100` + +### Filters And Sorting + +Many `find` endpoints support dynamic filters and Mongo-style operators. + +Common patterns: + + - `field[$regex]=text` + - `field[$options]=i` + - `date[gte]=2026-01-01` + - `date[lte]=2026-01-31` + - `privacy=true` + - `$sort[createdAt]=-1` + +### Quick Endpoint Map + +Public resources: + + - Account and auth: `/account` + - Catalogs: `/countries`, `/cities`, `/states`, `/meta-data`, `/meta-groups`, `/product-categories` + - Public business data: `/public-companies`, `/public-loads`, `/public-vehicles` + - Contact: `/contact-email` + +Private resources: + + - Company and members: `/companies`, `/users` + - Operations: `/loads`, `/load-templates`, `/proposals`, `/vehicles`, `/branches`, `/budgets` + - Files and alerts: `/load-attachments`, `/notifications` + - Company private list: `/groups` + ## Public endpoints Read registered resources: @@ -39,19 +109,19 @@ All these endpoints support the following parameters (except for those with `pub - `/` : List registered resources with pagination. - `/:id` : Read specific resource identified by Id. - - `/find?regex=xxx` : List resources that matches with regex (support pagination). + - `/find?regex=xxx` : List resources that match a regex (supports pagination). ### /account This endpoint provides mechanisms to register, login, recover password and renew JWT. -The __Login__ and __Renew__ process will return 2 tokens, the `accessToken` (JWT) which is used in every further request to authorize access to private endpoints. And the 2nd is the `session_token` (renew token), which should be used after the expiration of the JWT, in order to generate another _JWT_ and _session token_ without the need of use the _email_ and _password__. +The __Login__ and __Renew__ processes return 2 tokens: `accessToken` (JWT), used for private endpoint authorization, and `session_token` (renew token), used after JWT expiration to generate a new JWT and session token without sending email and password again. The _session token_ expiration is typically 30 days after its generation. Every renewal replaces the token in the DB and the expiration is reset again to 30 days. #### POST /account/authorize -Login process, returns a JWT and Renew Token +Login process, returns a JWT and renew token Expects a body with the following data: ```{.json} @@ -94,7 +164,7 @@ Returns: #### POST /account/signup -Create a new user. This will trigger an email with the OTP (one time password) to verify the email. There is no expiration time, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. +Create a new user. This triggers an email with an OTP (one-time password) to verify the email. There is no expiration time, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. This will return a checksum string to be used in the confirmation process. @@ -116,7 +186,7 @@ Returns: #### PATCH /account/signup Confirms registration of new user. This will trigger a welcome email to the user. -There is no timeout to confirm the email, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. +There is no timeout to confirm the email, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. If the checksum matches but the user is already registered, then this request will be rejected. @@ -140,7 +210,7 @@ Returns: #### POST /account/recover -Reset password request. This will trigger an email with the OTP (one time password) to verify the email. There is no expiration time, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. +Reset password request. This triggers an email with an OTP (one-time password) to verify the email. There is no expiration time, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. This will return a checksum string to be used in the confirmation process. @@ -162,7 +232,7 @@ Returns: #### PATCH /account/recover Confirms the email to recover the password. -There is no timeout to confirm the email, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. +There is no timeout to confirm the email, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. Expects a body with the same data as the POST request, but adding the OTP received in the email, and the checksum generated by the POST request. Here is an example: @@ -206,6 +276,7 @@ Returns: ### GET /public-companies Get public fields from registered companies. +This endpoint only returns non-private companies (`privacy=false` or missing). - `GET /shipper`: List registered shippers that are not hidden only. - `GET /carrier`: List registered carriers that are not hidden only. @@ -213,6 +284,7 @@ Get public fields from registered companies. ### GET /public-loads Get public fields from registered loads. +This endpoint only returns published, non-private loads (`privacy=false` or missing). - `GET /`: List only loads with status Published. @@ -236,7 +308,15 @@ Get public fields from registered vehicles. The following list of endpoints requires a JWT. - `GET /loads`: List loads related to my company. + - `GET /load-templates/all`: List load templates from my company. - `GET /load-attachments`: List load attachments related to my company or load id. + - `GET /groups/private`: Get the private group for my company. + +Recommended header for all private endpoint calls: + +```http +Authorization: Bearer +``` ### /public-load-tracking @@ -343,21 +423,57 @@ This endpoint is part of /loads endpoint - `GET /own` : Get own company data. - `PATCH /own` : Update own company data. - - `GET /shipper` : Get list of shipper companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city. + - `GET /shipper` : Get list of shipper companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city, privacy. - $sort[ field ] : -1/1 ; Sort result by field name - - `GET /carrier` : Get list of carrier companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city + - `GET /carrier` : Get list of carrier companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city, privacy. - $sort[ field ] : -1/1 ; Sort result by field name + - `privacy=true`: return only private companies that explicitly granted visibility to my company through groups. + - `privacy=false` or unset: return only non-private companies. - `GET /users/:companyId` : Get the list of users within a company. - `GET /:id` : Get data from specific company. +### /groups + + - `GET /private` : Get the private group list from my company. If it does not exist, it will be created. + - `PATCH /private/:id` : Add a company (company id) to the private group list. + - `DELETE /private/:id` : Remove a company (company id) from the private group list. + ### /load-attachments - `POST /loading/:id` : Upload/Update a loading attachment. - - `POST /downloading/:id` : Upload/Update a download attachment + - `POST /downloading/:id` : Upload/Update an unloading attachment. - `GET /load/:id` : Get the list of attachment ids from load. - `GET /:id` : Get attachment file. - `GET /` : Get attachment list from company. +### /load-templates + + - `POST /new` : Create a new load template for my company. + - `GET /all` : Get all load templates from my company. + - `GET /:id` : Get a single load template by id. + - `PATCH /:id` : Update a load template by id. + - `DELETE /:id` : Delete a load template by id. + +Fields accepted for create/update: + + - template_name + - alert_list + - origin_warehouse + - destination_warehouse + - origin + - origin_geo + - destination + - destination_geo + - categories + - product + - truck_type + - tyre_type + - weight + - estimated_cost + - distance + - actual_cost + - notes + ### /loads - `GET /find` : Find a list of elements with any of the following fields: @@ -381,10 +497,12 @@ This endpoint is part of /loads endpoint - est_unloading_date[lte] : Date less than. - company_name[$regex] : Regex string to find company_name - company_name[$options] : Regex options from MongoDB filter description + - privacy : `true` to list private loads shared with my company through groups; `false` or unset to list non-private loads. - $sort[ field ] : -1/1 ; Sort result by field name - alert_list: List of emails to notify whenever there is a change to the object. + - `GET /driver` : Find a list of loads for driver views with the same filter and pagination behavior used in `/find`. Doesn't take privacy into account (already handled by driver assignation). - `GET /calendar` : Find a list of elements with any of the following fields: - - date[gte] : Date grater than. + - date[gte] : Date greater than. - date[lte] : Date less than. - load_status : string enumerator ['Published', 'Loading', 'Transit', 'Downloading', 'Delivered']. - global : 1 To return calendar of all company, or 0/undefined for personal calendar only. @@ -444,7 +562,7 @@ Work In Progress __This endpoint is only valid for carriers.__ - - `GET /find` : Find a list of elements (from the company it self only) with any of the following fields: + - `GET /find` : Find a list of elements (from the company itself only) with any of the following fields: - is_available, - categories, - active_load, @@ -490,7 +608,7 @@ __This endpoint is only valid for carriers.__ ### Public -The following endpoints doesn't require a JWT authorization +The following endpoints do not require JWT authorization. #### observers/account/ diff --git a/v1/src/apps/private/companies/services.js b/v1/src/apps/private/companies/services.js index 9c398ec..7c2b1fc 100644 --- a/v1/src/apps/private/companies/services.js +++ b/v1/src/apps/private/companies/services.js @@ -1,11 +1,12 @@ "use strict"; const { ROOT_PATH, MODELS_PATH, HANDLERS_PATH, LIB_PATH } = process.env; -const { getModel } = require( `${ROOT_PATH}/${MODELS_PATH}` ); +const { getModel } = require( '../../../lib/Models' ); const { GenericHandler } = require( '../../../lib/Handlers/Generic.handler.js' ); -const { getPagination } = require( `${ROOT_PATH}/${LIB_PATH}/Misc.js` ); +const { getPagination } = require( '../../../lib/Misc.js' ); const usersModel = getModel('users'); const companiesModel = getModel('companies'); +const companyGroupsModel = getModel('company_groups'); const branchesModel = getModel('branches'); const vehiclesModel = getModel('vehicles'); const loadsModel = getModel('loads'); @@ -46,21 +47,60 @@ function getAndFilterList( query ){ return filter_list; } -async function getListByType( type , req ){ - const filter = { "company_type" : type , "is_hidden" : false }; - const select = [ - "rfc", - "company_name", - "company_type", - "company_code", - "company_city", - "company_state", - "createdAt", - "membership", - "categories", - "truck_type", - "company_description" - ]; +const companySelectField = +[ + "rfc", + "company_name", + "company_type", + "company_code", + "company_city", + "company_state", + "createdAt", + "membership", + "categories", + "truck_type", + "company_description", + "privacy" +]; +async function getCompanyIdListFromGroups( companyId ){ + const privateGroups = await companyGroupsModel.find({ + allowedCompanies: { $in: [companyId] } + }); + + if( !privateGroups ){ + return null; + } + + const companiesIds = privateGroups.map((group) => group.owner); + + return companiesIds; +} + +async function getListByType( companyId, type , req ){ + const { privacy } = req.query; + const privacyVal = ( privacy && ( privacy >= 1 || privacy.toLowerCase() === 'true' ))? true: false; + + let filter; + if( privacyVal ){ + const companiesIds = await getCompanyIdListFromGroups( companyId ) || []; + filter = { + _id : { $in : companiesIds }, + company_type: type, + is_hidden: false, + privacy: true, + } + }else{ + filter = { + company_type: type, + is_hidden: false, + $or : [ + { privacy : false }, + { privacy : { $exists : false } } + ] + }; + } + + const select = companySelectField; const { $sort } = req.query; const { elements , page } = getPagination( req.query ); let query_elements; @@ -143,7 +183,8 @@ async function getCompanyById( req , res ) { async function getListShippers( req , res ) { try{ - const retVal = await getListByType( "Shipper" , req ); + const companyId = req.context.companyId; + const retVal = await getListByType( companyId, "Shipper" , req ); res.send( retVal ); }catch( error ){ console.error( error ); @@ -153,7 +194,8 @@ async function getListShippers( req , res ) { async function getListCarriers( req , res ) { try{ - const retVal = await getListByType( "Carrier" , req ); + const companyId = req.context.companyId; + const retVal = await getListByType( companyId, "Carrier" , req ); res.send( retVal ); }catch( error ){ console.error( error ); diff --git a/v1/src/apps/private/groups/routes.js b/v1/src/apps/private/groups/routes.js new file mode 100644 index 0000000..ae29577 --- /dev/null +++ b/v1/src/apps/private/groups/routes.js @@ -0,0 +1,9 @@ +'use strict'; +const router = require('express').Router(); +const services= require('./services.js'); + +router.get('/private', services.getPrivateGroup); +router.patch('/private/:id', services.addCompanyToGroup); +router.delete('/private/:id', services.removeCompanyFromGroup); + +module.exports = router; diff --git a/v1/src/apps/private/groups/services.js b/v1/src/apps/private/groups/services.js new file mode 100644 index 0000000..076e282 --- /dev/null +++ b/v1/src/apps/private/groups/services.js @@ -0,0 +1,116 @@ +"use strict"; +const { getModel } = require( '../../../lib/Models' ); +const { getPagination } = require( '../../../lib/Misc.js' ); + +const CompanyModel = getModel('companies'); +const CompanyGroupModel = getModel('company_groups'); + +const company_projection = 'company_name rfc categories products truck_type company_city company_state'; + +async function getOrCreatePrivateGroup( owner ) { + let result = await CompanyGroupModel + .findOne({ owner: owner }) + .populate({ + path: 'allowedCompanies', + select: company_projection, + populate: { + path: 'categories', + select: '-_id name' + } + }); + + if( result ){ + return result; + } + + result = new CompanyGroupModel({ + owner: owner, + list_name: "private" + }); + await result.save(); + + return await CompanyGroupModel + .findOne({ owner: owner }) + .populate({ + path: 'allowedCompanies', + select: company_projection, + populate: { + path: 'categories', + select: '-_id name' + } + }); +} + +async function getPrivateGroup( req , res ) { + try{ + const companyId = req.context.companyId; + const result = await getOrCreatePrivateGroup( companyId ); + return res.send( result ); + }catch( error ){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function addCompanyToGroup(req, res){ + try{ + const companyId = req.context.companyId; + const newEntryId = req.params.id; + + if( newEntryId === companyId ){ + return res.status(400).send({ error: "can't add yourself" }); + } + + const company = await CompanyModel.findById( newEntryId ); + if( !company ){ + return res.status(400).send({ error: "invalid entry id" }); + } + + let result = await getOrCreatePrivateGroup( companyId ); + + /// Add to the list only if it is not there already + if( ! result.allowedCompanies.some( entry => entry.id === newEntryId ) ){ + await CompanyGroupModel.findByIdAndUpdate(result.id, { + $addToSet: { allowedCompanies: company.id } + }); + } + + result = await getOrCreatePrivateGroup( companyId ); + return res.status(200).send( result ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function removeCompanyFromGroup(req, res){ + try{ + const companyId = req.context.companyId; + const newEntryId = req.params.id; + + if( newEntryId === companyId ){ + return res.status(400).send({ error: "you are not at the list" }); + } + + let result = await getOrCreatePrivateGroup( companyId ); + + /// If the item belongs to the list remove it + if( result.allowedCompanies.some( entry => entry.id === newEntryId ) ){ + await CompanyGroupModel.findByIdAndUpdate(result.id, { + $pull: { allowedCompanies: newEntryId } + }); + } + + result = await getOrCreatePrivateGroup( companyId ); + return res.status(200).send( result ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +module.exports = { + getPrivateGroup, + addCompanyToGroup, + removeCompanyFromGroup +}; diff --git a/v1/src/apps/private/index.js b/v1/src/apps/private/index.js index 1f62546..0632130 100644 --- a/v1/src/apps/private/index.js +++ b/v1/src/apps/private/index.js @@ -13,10 +13,12 @@ const companies = require('./companies/routes.js'); const loadAttachments = require('./load-attachments/routes.js'); const loads = require('./loads/routes.js'); const loads_driver = require('./loads_driver/routes.js'); +const loadTemplates = require('./load-templates/routes.js'); const proposals = require('./proposals/routes.js'); const users = require('./users/routes.js'); const vehicles = require('./vehicles/routes.js'); const notifications = require('./notifications/routes.js'); +const company_groups = require('./groups/routes.js'); router.use( jwtValidator.middleware ); router.use( context.middleware ); @@ -25,25 +27,14 @@ router.use('/account', account); router.use('/budgets', budgets); router.use('/branches', branches); router.use('/companies', companies); +router.use('/groups', company_groups); router.use('/load-attachments', loadAttachments ); router.use('/loads', loads); router.use('/loads_driver', loads_driver); +router.use('/load-templates', loadTemplates ); router.use('/notifications', notifications); router.use('/proposals', proposals); router.use('/users', users); router.use('/vehicles', vehicles); -/* -router.use('/orders', test); -router.use('/mailer', test); -router.use('/memberships', test); -router.use('/bootresolvers', test); -router.use('/news', test); -router.use('/branches', test); -router.use('/trackings', test); -router.use('/upload', test); -router.use('/calendars', test); -router.use('/dashboard', test); -*/ - module.exports = router; diff --git a/v1/src/apps/private/load-templates/routes.js b/v1/src/apps/private/load-templates/routes.js new file mode 100644 index 0000000..38a2605 --- /dev/null +++ b/v1/src/apps/private/load-templates/routes.js @@ -0,0 +1,11 @@ +'use strict'; +const router = require('express').Router(); +const services= require('./services.js'); + +router.post('/new', services.createTemplate); +router.get('/all', services.getAllTemplates); +router.get('/:id', services.getTemplate); +router.patch('/:id', services.updateTemplate); +router.delete('/:id', services.deleteTemplate); + +module.exports = router; diff --git a/v1/src/apps/private/load-templates/services.js b/v1/src/apps/private/load-templates/services.js new file mode 100644 index 0000000..56a5882 --- /dev/null +++ b/v1/src/apps/private/load-templates/services.js @@ -0,0 +1,137 @@ +"use strict"; +const { getModel } = require( '../../../lib/Models' ); +const { getPagination } = require( '../../../lib/Misc.js' ); + +const LoadTemplatesModel = getModel('load_templates'); + +function cleanUpData( data ){ + /** + * Take the only fields from model that + * should be modifiable by the client. + * The rest are populated on demand by the event handlers. + */ + let data_fields = { + template_name: null, + alert_list: null, + origin_warehouse: null, + destination_warehouse: null, + origin: null, + origin_geo: null, + destination: null, + destination_geo: null, + categories: null, + product: null, + truck_type: null, + tyre_type: null, + weight: null, + estimated_cost: null, + distance: null, + actual_cost: null, + notes: null, + }; + + let filtered_data = {}; + + if( Object.keys( data_fields ).length === 0 ){ + throw "no data to add"; + } + + for ( const [key, value] of Object.entries( data_fields ) ) { + if( Object.hasOwn( data, key ) ){ + filtered_data[ key ] = data[ key ]; + } + } + + if( Object.keys( filtered_data ).length === 0 ){ + throw "no data to add"; + } + + return filtered_data; +} + +async function createTemplate( req , res ) { + try{ + const companyId = req.context.companyId; + + const data = cleanUpData( req.body ); + data.company = companyId; + + const template = new LoadTemplatesModel( data ); + await template.save(); + + res.send( template ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function updateTemplate( req , res ) { + try{ + const templateId = req.params.id; + const companyId = req.context.companyId; + + const data = cleanUpData( req.body ); + data.company = companyId; + + await LoadTemplatesModel.findByIdAndUpdate( templateId , data ); + + const template = await LoadTemplatesModel.findById( templateId ); + res.send( template ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function getTemplate( req, res ){ + try{ + const templateId = req.params.id; + const template = await LoadTemplatesModel.findById( templateId ); + + if( ! template ){ + return res.status(400).send({ error : "invalid template id"}); + } + + res.send( template ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function deleteTemplate( req, res ){ + try{ + const companyId = req.context.companyId; + const templateId = req.params.id; + await LoadTemplatesModel.findOneAndDelete({ + _id: templateId, + company : companyId + }); + res.send({ msg: "item removed successfully" }); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function getAllTemplates( req, res ){ + try{ + const companyId = req.context.companyId; + const templateList = await LoadTemplatesModel.find( { + company : companyId + } ); + res.send( templateList ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +module.exports = { + createTemplate, + getTemplate, + getAllTemplates, + updateTemplate, + deleteTemplate, +}; diff --git a/v1/src/apps/private/loads/routes.js b/v1/src/apps/private/loads/routes.js index 038099a..492b28b 100644 --- a/v1/src/apps/private/loads/routes.js +++ b/v1/src/apps/private/loads/routes.js @@ -3,6 +3,7 @@ const router = require('express').Router(); const services= require('./services.js'); router.get('/find', services.findList); +router.get('/driver', services.findDriverList); router.get('/calendar', services.findCalendarList); router.post('/new', services.postLoad); diff --git a/v1/src/apps/private/loads/services.js b/v1/src/apps/private/loads/services.js index 9d9b0f5..0512d1a 100644 --- a/v1/src/apps/private/loads/services.js +++ b/v1/src/apps/private/loads/services.js @@ -7,6 +7,7 @@ const Model = getModel('loads'); const CompanyModel = getModel('companies'); const ProposalsModel = getModel('proposals'); const branchesModel = getModel('branches'); +const companyGroupsModel = getModel('company_groups'); const carrier_projection = [ 'company_name', @@ -141,7 +142,21 @@ function getAndFilterList( query ){ return filter_list; } -async function findLoads( query ){ +async function getCompanyIdListFromGroups( companyId ){ + const privateGroups = await companyGroupsModel.find({ + allowedCompanies: { $in: [companyId] } + }); + + if( !privateGroups ){ + return null; + } + + const companiesIds = privateGroups.map((group) => group.owner); + + return companiesIds; +} + +async function findDriverLoads( query ){ const { $sort, company_name } = query; const { page, elements } = getPagination( query ); const andFilterList = getAndFilterList( query ) || []; @@ -184,6 +199,65 @@ async function findLoads( query ){ }; } +async function findLoads( companyId, query ){ + const { $sort, company_name } = query; + const { page, elements } = getPagination( query ); + const andFilterList = getAndFilterList( query ) || []; + + const { privacy } = query; + const privacyVal = ( privacy && ( privacy >= 1 || privacy.toLowerCase() === 'true' ))? true: false; + + let filter; + + if( privacyVal ){ + const companiesIds = await getCompanyIdListFromGroups( companyId ) || []; + filter = { + company : { $in : companiesIds }, + privacy: true, + } + }else{ + filter = { + $or : [ + { privacy : false }, + { privacy : { $exists : false } } + ] + } + } + + if( company_name ){ + /* Populate list of company ids with match on the company_name */ + const company_list = await CompanyModel.find( { company_name }, [ "id" ] ); + const or_company_list = [] + company_list.forEach( (item) =>{ + or_company_list.push({"company" : item.id}); + }) + andFilterList.push({ + $or : or_company_list + }); + } + + if( andFilterList.length > 0 ){ + filter.$and = andFilterList; + } + + const { total , limit, skip, data } = await generic.getList( page , elements, filter, null, $sort ); + + const load_list = data; + + for(let i=0; i { const findList = async(req, res) => { try{ + const companyId = req.context.companyId; const query = req.query || {}; - const retVal = await findLoads( query ); + const retVal = await findLoads( companyId, query ); res.send( retVal ); }catch(error){ console.error( error ); @@ -290,6 +365,17 @@ const findList = async(req, res) => { } }; +const findDriverList = async ( req, res ) => { + try{ + const query = req.query || {}; + const retVal = await findDriverLoads( companyId, query ); + res.send( retVal ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + const getById = async(req, res) => { try{ const elementId = req.params.id; @@ -356,6 +442,7 @@ function getDataToModify( data ){ notes : null, payment_term : null, terms_and_conditions : null, + privacy: null }; let filtered_data = {}; @@ -523,4 +610,4 @@ const deleteLoad = async(req, res) => { } }; -module.exports = { findCalendarList, findList, getById, patchLoad, postLoad, deleteLoad }; +module.exports = { findCalendarList, findList, findDriverList, getById, patchLoad, postLoad, deleteLoad }; diff --git a/v1/src/apps/public/public-companies/services.js b/v1/src/apps/public/public-companies/services.js index c424ef6..c15c9c3 100644 --- a/v1/src/apps/public/public-companies/services.js +++ b/v1/src/apps/public/public-companies/services.js @@ -46,7 +46,14 @@ function getAndFilterList( query ){ } async function getListByType( type , req ){ - const filter = { "company_type" : type , "is_hidden" : false }; + const filter = { + company_type: type, + is_hidden: false, + $or : [ + { privacy : false }, + { privacy : { $exists : false } } + ] + }; const select = [ "rfc", "company_name", @@ -58,7 +65,8 @@ async function getListByType( type , req ){ "membership", "categories", "truck_type", - "company_description" + "company_description", + "privacy" ]; const { elements } = getPagination( req.query ); const page = 0;// No pagination allowed to this endpoint diff --git a/v1/src/apps/public/public-loads/services.js b/v1/src/apps/public/public-loads/services.js index f1cc0be..2bd3290 100644 --- a/v1/src/apps/public/public-loads/services.js +++ b/v1/src/apps/public/public-loads/services.js @@ -14,7 +14,13 @@ const generic = new GenericHandler( Model, null, populate_list ); const getList = async(req, res) => { try{ - const filter = { status : "Published" }; + const filter = { + status: "Published", + $or : [ + { privacy : false }, + { privacy : { $exists : false } } + ] + }; const select = [ "shipment_code", "categories", @@ -26,7 +32,8 @@ const getList = async(req, res) => { "est_loading_date", "est_unloading_date", "origin", - "destination" + "destination", + "privacy" ]; const { page , elements } = getPagination( req.query ); const retVal = await generic.getList(page , elements, filter, select ); diff --git a/v1/src/lib/Handlers/MailClient/StandAlone.handler.js b/v1/src/lib/Handlers/MailClient/StandAlone.handler.js index 79e9bee..3b93f58 100644 --- a/v1/src/lib/Handlers/MailClient/StandAlone.handler.js +++ b/v1/src/lib/Handlers/MailClient/StandAlone.handler.js @@ -53,7 +53,7 @@ async function sendMailTemplate( receiver, subject, html ){ async function StandAloneContactEmail( content ){ /** Send out the contact email to the default list of people */ - const receiver_list = ["support@etaviaporte.com", "eduardo.hernandez@etaviaporte.com"]; + const receiver_list = ["eduardo.hernandez@etaviaporte.com","abel.mejia@etaviaporte.com"]; const {name, email, message } = content; for(const receiver of receiver_list){ await transporter.sendMail({ diff --git a/v1/src/lib/Models/companies.model.js b/v1/src/lib/Models/companies.model.js index 76bbc17..222e1f9 100644 --- a/v1/src/lib/Models/companies.model.js +++ b/v1/src/lib/Models/companies.model.js @@ -59,6 +59,7 @@ const schema = new Schema({ lng: { type: String }, is_hidden: { type: Boolean, default: false }, + privacy: { type: Boolean, default: false }, /// Disables visibility on the directory, only enabled for private groups. createdAt: { type : Date, required : true, default : () => { return Date.now(); } } }); diff --git a/v1/src/lib/Models/company_groups.models.js b/v1/src/lib/Models/company_groups.models.js new file mode 100644 index 0000000..d4296b1 --- /dev/null +++ b/v1/src/lib/Models/company_groups.models.js @@ -0,0 +1,12 @@ +const mongoose = require('mongoose'); +const { Schema } = mongoose; + +const schema = new Schema({ + owner: { type: Schema.Types.ObjectId, ref: 'companies', required: true }, /// Company owner of this list + list_name: { type: String }, /// Name of the list + allowedCompanies: [{ type: Schema.Types.ObjectId, ref: 'companies' }], /// Behaves as a tuple, not repeated elements + createdAt: { type : Date, required : true, default : () => { return Date.now(); } }, + updatedAt: { type : Date, required : true, default : () => { return Date.now(); } } +}); + +module.exports = mongoose.model( "companygroups", schema ); diff --git a/v1/src/lib/Models/index.js b/v1/src/lib/Models/index.js index 5feef98..76345d8 100644 --- a/v1/src/lib/Models/index.js +++ b/v1/src/lib/Models/index.js @@ -4,9 +4,11 @@ const branches = require('./branches.model.js'); const budgets = require('./budgets.model.js'); const cities = require('./cities.model.js'); const companies = require('./companies.model.js'); +const companygroups = require('./company_groups.models.js') const countries = require('./countries.model.js'); const load_attachments = require('./load-attachments.model.js'); const loads = require('./loads.model.js'); +const loadtemplates = require('./load_templates.model.js'); const mailer = require('./mailer.model.js'); const memberships = require('./memberships.model.js'); const meta_data = require('./meta-data.model.js'); @@ -34,12 +36,16 @@ function getModel( name ){ return cities; case 'companies': return companies; + case 'company_groups': + return companygroups; case 'countries': return countries; case 'load_attachments': return load_attachments; case 'loads': return loads; + case 'load_templates': + return loadtemplates; case 'mailer': return mailer; case 'memberships': diff --git a/v1/src/lib/Models/load_templates.model.js b/v1/src/lib/Models/load_templates.model.js new file mode 100644 index 0000000..400f353 --- /dev/null +++ b/v1/src/lib/Models/load_templates.model.js @@ -0,0 +1,68 @@ +const mongoose = require('mongoose'); +const { Schema } = mongoose; + +const address = new Schema({ + company_name: { type: String }, + + street_address1: { type: String }, + street_address2: { type: String }, + city: { type: String }, + state: { type: String }, + country: { type: String }, + zipcode: { type: String }, + + landmark: { type: String }, + + lat: { type: String }, + lng: { type: String }, +}); + + +const pointSchema = new Schema({ + type: { + type: String, + enum: ['Point'], + required: true + }, + coordinates: { + type: [Number], + required: true + } +}); + + +const schema = new Schema({ + company: { type: Schema.Types.ObjectId, ref: 'companies', required: true }, + template_name : { type: String }, + + alert_list: [{ type: String, lowercase: true }], + + origin_warehouse : { type: Schema.Types.ObjectId, ref: 'branches' }, + destination_warehouse : { type: Schema.Types.ObjectId, ref: 'branches' }, + + origin: address, + origin_geo: { + type: pointSchema, + }, + destination: address, + destination_geo: { + type: pointSchema, + }, + + categories: [{ type: Schema.Types.ObjectId, ref: 'productcategories' }], + product: { type: Schema.Types.ObjectId, ref: 'products' }, + + truck_type: { type: String }, + tyre_type: { type: String }, + weight: { type: Number }, + estimated_cost: { type: Number }, + + distance: { type: Number }, + actual_cost: { type: Number }, + notes: { type: String }, + + created_by: { type: Schema.Types.ObjectId, ref: 'users' }, // shipper + createdAt: { type : Date, required : true, default : () => { return Date.now(); } } +}); + +module.exports = mongoose.model( "loadtemplates", schema ); diff --git a/v1/src/lib/Models/loads.model.js b/v1/src/lib/Models/loads.model.js index 988d3eb..6c9fc02 100644 --- a/v1/src/lib/Models/loads.model.js +++ b/v1/src/lib/Models/loads.model.js @@ -90,7 +90,9 @@ const schema = new Schema({ payment_term: { type: String }, terms_and_conditions: { type: String }, - createdAt: { type : Date, required : true, default : () => { return Date.now(); } } + createdAt: { type : Date, required : true, default : () => { return Date.now(); } }, + + privacy: { type: Boolean, default: false }, /// Disables visibility on the directory, only enabled for private groups. }); module.exports = mongoose.model( "loads", schema ); From f79329e714b214aa75173b14db44a45995a095e5 Mon Sep 17 00:00:00 2001 From: Josepablo Cruz Baas Date: Tue, 31 Mar 2026 22:06:21 +0000 Subject: [PATCH 2/3] Draft: Revert "feat: Private Groups + Load-Templates" --- .gitignore | 3 +- v1/README.md | 144 ++---------------- v1/src/apps/private/companies/services.js | 80 +++------- v1/src/apps/private/groups/routes.js | 9 -- v1/src/apps/private/groups/services.js | 116 -------------- v1/src/apps/private/index.js | 17 ++- v1/src/apps/private/load-templates/routes.js | 11 -- .../apps/private/load-templates/services.js | 137 ----------------- v1/src/apps/private/loads/routes.js | 1 - v1/src/apps/private/loads/services.js | 93 +---------- .../apps/public/public-companies/services.js | 12 +- v1/src/apps/public/public-loads/services.js | 11 +- .../Handlers/MailClient/StandAlone.handler.js | 2 +- v1/src/lib/Models/companies.model.js | 1 - v1/src/lib/Models/company_groups.models.js | 12 -- v1/src/lib/Models/index.js | 6 - v1/src/lib/Models/load_templates.model.js | 68 --------- v1/src/lib/Models/loads.model.js | 4 +- 18 files changed, 55 insertions(+), 672 deletions(-) delete mode 100644 v1/src/apps/private/groups/routes.js delete mode 100644 v1/src/apps/private/groups/services.js delete mode 100644 v1/src/apps/private/load-templates/routes.js delete mode 100644 v1/src/apps/private/load-templates/services.js delete mode 100644 v1/src/lib/Models/company_groups.models.js delete mode 100644 v1/src/lib/Models/load_templates.model.js diff --git a/.gitignore b/.gitignore index 20bd1d0..ffc83f1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ **/migrate.js **/scripts/migrate/* v1/src/config/*.json -**/apiConfig.json -ignore/* +**/apiConfig.json \ No newline at end of file diff --git a/v1/README.md b/v1/README.md index 6576e3e..2e1b1cb 100644 --- a/v1/README.md +++ b/v1/README.md @@ -7,11 +7,6 @@ ETA Viaporte API - NodeJS v18 - Docker -# Audience - -This document is oriented to front-end developers integrating with the ETA API. -Use the Quick Start section first, then go to the endpoint section you need. - # Endpoints All endpoints that return a list of elements is paginable with the following queries: @@ -23,71 +18,6 @@ Example: - `/endpoint?page=2` : Get page 2 with default (10) elements per page. - `/endpoint?elements=50` : Get page 0 with 50 elements. -## Quick Start For Front-End Integration - -### Authentication - -Private endpoints require JWT authorization. - -1. Login with `POST /account/authorize`. -2. Store `accessToken` and `session_token`. -3. Send JWT in request headers: - -```http -Authorization: Bearer -``` - -4. When JWT expires, request a new token with `GET /account/authorize/:session_token`. - -### Account Lifecycle (Common Front-End Flow) - -1. Signup request: `POST /account/signup` returns `checksum`. -2. Signup confirmation: `PATCH /account/signup` with `otp` + `checksum`. -3. Login: `POST /account/authorize`. -4. Register company profile: `POST /account/register`. - -### Pagination - -Pagination is available on list endpoints using: - - - `elements`: page size - - `page`: page index (starts at 0) - -Examples: - - - `/loads?elements=20&page=0` - - `/loads?page=1` - - `/loads?elements=100` - -### Filters And Sorting - -Many `find` endpoints support dynamic filters and Mongo-style operators. - -Common patterns: - - - `field[$regex]=text` - - `field[$options]=i` - - `date[gte]=2026-01-01` - - `date[lte]=2026-01-31` - - `privacy=true` - - `$sort[createdAt]=-1` - -### Quick Endpoint Map - -Public resources: - - - Account and auth: `/account` - - Catalogs: `/countries`, `/cities`, `/states`, `/meta-data`, `/meta-groups`, `/product-categories` - - Public business data: `/public-companies`, `/public-loads`, `/public-vehicles` - - Contact: `/contact-email` - -Private resources: - - - Company and members: `/companies`, `/users` - - Operations: `/loads`, `/load-templates`, `/proposals`, `/vehicles`, `/branches`, `/budgets` - - Files and alerts: `/load-attachments`, `/notifications` - - Company private list: `/groups` - ## Public endpoints Read registered resources: @@ -109,19 +39,19 @@ All these endpoints support the following parameters (except for those with `pub - `/` : List registered resources with pagination. - `/:id` : Read specific resource identified by Id. - - `/find?regex=xxx` : List resources that match a regex (supports pagination). + - `/find?regex=xxx` : List resources that matches with regex (support pagination). ### /account This endpoint provides mechanisms to register, login, recover password and renew JWT. -The __Login__ and __Renew__ processes return 2 tokens: `accessToken` (JWT), used for private endpoint authorization, and `session_token` (renew token), used after JWT expiration to generate a new JWT and session token without sending email and password again. +The __Login__ and __Renew__ process will return 2 tokens, the `accessToken` (JWT) which is used in every further request to authorize access to private endpoints. And the 2nd is the `session_token` (renew token), which should be used after the expiration of the JWT, in order to generate another _JWT_ and _session token_ without the need of use the _email_ and _password__. The _session token_ expiration is typically 30 days after its generation. Every renewal replaces the token in the DB and the expiration is reset again to 30 days. #### POST /account/authorize -Login process, returns a JWT and renew token +Login process, returns a JWT and Renew Token Expects a body with the following data: ```{.json} @@ -164,7 +94,7 @@ Returns: #### POST /account/signup -Create a new user. This triggers an email with an OTP (one-time password) to verify the email. There is no expiration time, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. +Create a new user. This will trigger an email with the OTP (one time password) to verify the email. There is no expiration time, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. This will return a checksum string to be used in the confirmation process. @@ -186,7 +116,7 @@ Returns: #### PATCH /account/signup Confirms registration of new user. This will trigger a welcome email to the user. -There is no timeout to confirm the email, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. +There is no timeout to confirm the email, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. If the checksum matches but the user is already registered, then this request will be rejected. @@ -210,7 +140,7 @@ Returns: #### POST /account/recover -Reset password request. This triggers an email with an OTP (one-time password) to verify the email. There is no expiration time, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. +Reset password request. This will trigger an email with the OTP (one time password) to verify the email. There is no expiration time, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. This will return a checksum string to be used in the confirmation process. @@ -232,7 +162,7 @@ Returns: #### PATCH /account/recover Confirms the email to recover the password. -There is no timeout to confirm the email, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. +There is no timeout to confirm the email, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. Expects a body with the same data as the POST request, but adding the OTP received in the email, and the checksum generated by the POST request. Here is an example: @@ -276,7 +206,6 @@ Returns: ### GET /public-companies Get public fields from registered companies. -This endpoint only returns non-private companies (`privacy=false` or missing). - `GET /shipper`: List registered shippers that are not hidden only. - `GET /carrier`: List registered carriers that are not hidden only. @@ -284,7 +213,6 @@ This endpoint only returns non-private companies (`privacy=false` or missing). ### GET /public-loads Get public fields from registered loads. -This endpoint only returns published, non-private loads (`privacy=false` or missing). - `GET /`: List only loads with status Published. @@ -308,15 +236,7 @@ Get public fields from registered vehicles. The following list of endpoints requires a JWT. - `GET /loads`: List loads related to my company. - - `GET /load-templates/all`: List load templates from my company. - `GET /load-attachments`: List load attachments related to my company or load id. - - `GET /groups/private`: Get the private group for my company. - -Recommended header for all private endpoint calls: - -```http -Authorization: Bearer -``` ### /public-load-tracking @@ -423,57 +343,21 @@ This endpoint is part of /loads endpoint - `GET /own` : Get own company data. - `PATCH /own` : Update own company data. - - `GET /shipper` : Get list of shipper companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city, privacy. + - `GET /shipper` : Get list of shipper companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city. - $sort[ field ] : -1/1 ; Sort result by field name - - `GET /carrier` : Get list of carrier companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city, privacy. + - `GET /carrier` : Get list of carrier companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city - $sort[ field ] : -1/1 ; Sort result by field name - - `privacy=true`: return only private companies that explicitly granted visibility to my company through groups. - - `privacy=false` or unset: return only non-private companies. - `GET /users/:companyId` : Get the list of users within a company. - `GET /:id` : Get data from specific company. -### /groups - - - `GET /private` : Get the private group list from my company. If it does not exist, it will be created. - - `PATCH /private/:id` : Add a company (company id) to the private group list. - - `DELETE /private/:id` : Remove a company (company id) from the private group list. - ### /load-attachments - `POST /loading/:id` : Upload/Update a loading attachment. - - `POST /downloading/:id` : Upload/Update an unloading attachment. + - `POST /downloading/:id` : Upload/Update a download attachment - `GET /load/:id` : Get the list of attachment ids from load. - `GET /:id` : Get attachment file. - `GET /` : Get attachment list from company. -### /load-templates - - - `POST /new` : Create a new load template for my company. - - `GET /all` : Get all load templates from my company. - - `GET /:id` : Get a single load template by id. - - `PATCH /:id` : Update a load template by id. - - `DELETE /:id` : Delete a load template by id. - -Fields accepted for create/update: - - - template_name - - alert_list - - origin_warehouse - - destination_warehouse - - origin - - origin_geo - - destination - - destination_geo - - categories - - product - - truck_type - - tyre_type - - weight - - estimated_cost - - distance - - actual_cost - - notes - ### /loads - `GET /find` : Find a list of elements with any of the following fields: @@ -497,12 +381,10 @@ Fields accepted for create/update: - est_unloading_date[lte] : Date less than. - company_name[$regex] : Regex string to find company_name - company_name[$options] : Regex options from MongoDB filter description - - privacy : `true` to list private loads shared with my company through groups; `false` or unset to list non-private loads. - $sort[ field ] : -1/1 ; Sort result by field name - alert_list: List of emails to notify whenever there is a change to the object. - - `GET /driver` : Find a list of loads for driver views with the same filter and pagination behavior used in `/find`. Doesn't take privacy into account (already handled by driver assignation). - `GET /calendar` : Find a list of elements with any of the following fields: - - date[gte] : Date greater than. + - date[gte] : Date grater than. - date[lte] : Date less than. - load_status : string enumerator ['Published', 'Loading', 'Transit', 'Downloading', 'Delivered']. - global : 1 To return calendar of all company, or 0/undefined for personal calendar only. @@ -562,7 +444,7 @@ Work In Progress __This endpoint is only valid for carriers.__ - - `GET /find` : Find a list of elements (from the company itself only) with any of the following fields: + - `GET /find` : Find a list of elements (from the company it self only) with any of the following fields: - is_available, - categories, - active_load, @@ -608,7 +490,7 @@ __This endpoint is only valid for carriers.__ ### Public -The following endpoints do not require JWT authorization. +The following endpoints doesn't require a JWT authorization #### observers/account/ diff --git a/v1/src/apps/private/companies/services.js b/v1/src/apps/private/companies/services.js index 7c2b1fc..9c398ec 100644 --- a/v1/src/apps/private/companies/services.js +++ b/v1/src/apps/private/companies/services.js @@ -1,12 +1,11 @@ "use strict"; const { ROOT_PATH, MODELS_PATH, HANDLERS_PATH, LIB_PATH } = process.env; -const { getModel } = require( '../../../lib/Models' ); +const { getModel } = require( `${ROOT_PATH}/${MODELS_PATH}` ); const { GenericHandler } = require( '../../../lib/Handlers/Generic.handler.js' ); -const { getPagination } = require( '../../../lib/Misc.js' ); +const { getPagination } = require( `${ROOT_PATH}/${LIB_PATH}/Misc.js` ); const usersModel = getModel('users'); const companiesModel = getModel('companies'); -const companyGroupsModel = getModel('company_groups'); const branchesModel = getModel('branches'); const vehiclesModel = getModel('vehicles'); const loadsModel = getModel('loads'); @@ -47,60 +46,21 @@ function getAndFilterList( query ){ return filter_list; } -const companySelectField = -[ - "rfc", - "company_name", - "company_type", - "company_code", - "company_city", - "company_state", - "createdAt", - "membership", - "categories", - "truck_type", - "company_description", - "privacy" -]; -async function getCompanyIdListFromGroups( companyId ){ - const privateGroups = await companyGroupsModel.find({ - allowedCompanies: { $in: [companyId] } - }); - - if( !privateGroups ){ - return null; - } - - const companiesIds = privateGroups.map((group) => group.owner); - - return companiesIds; -} - -async function getListByType( companyId, type , req ){ - const { privacy } = req.query; - const privacyVal = ( privacy && ( privacy >= 1 || privacy.toLowerCase() === 'true' ))? true: false; - - let filter; - if( privacyVal ){ - const companiesIds = await getCompanyIdListFromGroups( companyId ) || []; - filter = { - _id : { $in : companiesIds }, - company_type: type, - is_hidden: false, - privacy: true, - } - }else{ - filter = { - company_type: type, - is_hidden: false, - $or : [ - { privacy : false }, - { privacy : { $exists : false } } - ] - }; - } - - const select = companySelectField; +async function getListByType( type , req ){ + const filter = { "company_type" : type , "is_hidden" : false }; + const select = [ + "rfc", + "company_name", + "company_type", + "company_code", + "company_city", + "company_state", + "createdAt", + "membership", + "categories", + "truck_type", + "company_description" + ]; const { $sort } = req.query; const { elements , page } = getPagination( req.query ); let query_elements; @@ -183,8 +143,7 @@ async function getCompanyById( req , res ) { async function getListShippers( req , res ) { try{ - const companyId = req.context.companyId; - const retVal = await getListByType( companyId, "Shipper" , req ); + const retVal = await getListByType( "Shipper" , req ); res.send( retVal ); }catch( error ){ console.error( error ); @@ -194,8 +153,7 @@ async function getListShippers( req , res ) { async function getListCarriers( req , res ) { try{ - const companyId = req.context.companyId; - const retVal = await getListByType( companyId, "Carrier" , req ); + const retVal = await getListByType( "Carrier" , req ); res.send( retVal ); }catch( error ){ console.error( error ); diff --git a/v1/src/apps/private/groups/routes.js b/v1/src/apps/private/groups/routes.js deleted file mode 100644 index ae29577..0000000 --- a/v1/src/apps/private/groups/routes.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; -const router = require('express').Router(); -const services= require('./services.js'); - -router.get('/private', services.getPrivateGroup); -router.patch('/private/:id', services.addCompanyToGroup); -router.delete('/private/:id', services.removeCompanyFromGroup); - -module.exports = router; diff --git a/v1/src/apps/private/groups/services.js b/v1/src/apps/private/groups/services.js deleted file mode 100644 index 076e282..0000000 --- a/v1/src/apps/private/groups/services.js +++ /dev/null @@ -1,116 +0,0 @@ -"use strict"; -const { getModel } = require( '../../../lib/Models' ); -const { getPagination } = require( '../../../lib/Misc.js' ); - -const CompanyModel = getModel('companies'); -const CompanyGroupModel = getModel('company_groups'); - -const company_projection = 'company_name rfc categories products truck_type company_city company_state'; - -async function getOrCreatePrivateGroup( owner ) { - let result = await CompanyGroupModel - .findOne({ owner: owner }) - .populate({ - path: 'allowedCompanies', - select: company_projection, - populate: { - path: 'categories', - select: '-_id name' - } - }); - - if( result ){ - return result; - } - - result = new CompanyGroupModel({ - owner: owner, - list_name: "private" - }); - await result.save(); - - return await CompanyGroupModel - .findOne({ owner: owner }) - .populate({ - path: 'allowedCompanies', - select: company_projection, - populate: { - path: 'categories', - select: '-_id name' - } - }); -} - -async function getPrivateGroup( req , res ) { - try{ - const companyId = req.context.companyId; - const result = await getOrCreatePrivateGroup( companyId ); - return res.send( result ); - }catch( error ){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -} - -async function addCompanyToGroup(req, res){ - try{ - const companyId = req.context.companyId; - const newEntryId = req.params.id; - - if( newEntryId === companyId ){ - return res.status(400).send({ error: "can't add yourself" }); - } - - const company = await CompanyModel.findById( newEntryId ); - if( !company ){ - return res.status(400).send({ error: "invalid entry id" }); - } - - let result = await getOrCreatePrivateGroup( companyId ); - - /// Add to the list only if it is not there already - if( ! result.allowedCompanies.some( entry => entry.id === newEntryId ) ){ - await CompanyGroupModel.findByIdAndUpdate(result.id, { - $addToSet: { allowedCompanies: company.id } - }); - } - - result = await getOrCreatePrivateGroup( companyId ); - return res.status(200).send( result ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -} - -async function removeCompanyFromGroup(req, res){ - try{ - const companyId = req.context.companyId; - const newEntryId = req.params.id; - - if( newEntryId === companyId ){ - return res.status(400).send({ error: "you are not at the list" }); - } - - let result = await getOrCreatePrivateGroup( companyId ); - - /// If the item belongs to the list remove it - if( result.allowedCompanies.some( entry => entry.id === newEntryId ) ){ - await CompanyGroupModel.findByIdAndUpdate(result.id, { - $pull: { allowedCompanies: newEntryId } - }); - } - - result = await getOrCreatePrivateGroup( companyId ); - return res.status(200).send( result ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -} - -module.exports = { - getPrivateGroup, - addCompanyToGroup, - removeCompanyFromGroup -}; diff --git a/v1/src/apps/private/index.js b/v1/src/apps/private/index.js index 0632130..1f62546 100644 --- a/v1/src/apps/private/index.js +++ b/v1/src/apps/private/index.js @@ -13,12 +13,10 @@ const companies = require('./companies/routes.js'); const loadAttachments = require('./load-attachments/routes.js'); const loads = require('./loads/routes.js'); const loads_driver = require('./loads_driver/routes.js'); -const loadTemplates = require('./load-templates/routes.js'); const proposals = require('./proposals/routes.js'); const users = require('./users/routes.js'); const vehicles = require('./vehicles/routes.js'); const notifications = require('./notifications/routes.js'); -const company_groups = require('./groups/routes.js'); router.use( jwtValidator.middleware ); router.use( context.middleware ); @@ -27,14 +25,25 @@ router.use('/account', account); router.use('/budgets', budgets); router.use('/branches', branches); router.use('/companies', companies); -router.use('/groups', company_groups); router.use('/load-attachments', loadAttachments ); router.use('/loads', loads); router.use('/loads_driver', loads_driver); -router.use('/load-templates', loadTemplates ); router.use('/notifications', notifications); router.use('/proposals', proposals); router.use('/users', users); router.use('/vehicles', vehicles); +/* +router.use('/orders', test); +router.use('/mailer', test); +router.use('/memberships', test); +router.use('/bootresolvers', test); +router.use('/news', test); +router.use('/branches', test); +router.use('/trackings', test); +router.use('/upload', test); +router.use('/calendars', test); +router.use('/dashboard', test); +*/ + module.exports = router; diff --git a/v1/src/apps/private/load-templates/routes.js b/v1/src/apps/private/load-templates/routes.js deleted file mode 100644 index 38a2605..0000000 --- a/v1/src/apps/private/load-templates/routes.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -const router = require('express').Router(); -const services= require('./services.js'); - -router.post('/new', services.createTemplate); -router.get('/all', services.getAllTemplates); -router.get('/:id', services.getTemplate); -router.patch('/:id', services.updateTemplate); -router.delete('/:id', services.deleteTemplate); - -module.exports = router; diff --git a/v1/src/apps/private/load-templates/services.js b/v1/src/apps/private/load-templates/services.js deleted file mode 100644 index 56a5882..0000000 --- a/v1/src/apps/private/load-templates/services.js +++ /dev/null @@ -1,137 +0,0 @@ -"use strict"; -const { getModel } = require( '../../../lib/Models' ); -const { getPagination } = require( '../../../lib/Misc.js' ); - -const LoadTemplatesModel = getModel('load_templates'); - -function cleanUpData( data ){ - /** - * Take the only fields from model that - * should be modifiable by the client. - * The rest are populated on demand by the event handlers. - */ - let data_fields = { - template_name: null, - alert_list: null, - origin_warehouse: null, - destination_warehouse: null, - origin: null, - origin_geo: null, - destination: null, - destination_geo: null, - categories: null, - product: null, - truck_type: null, - tyre_type: null, - weight: null, - estimated_cost: null, - distance: null, - actual_cost: null, - notes: null, - }; - - let filtered_data = {}; - - if( Object.keys( data_fields ).length === 0 ){ - throw "no data to add"; - } - - for ( const [key, value] of Object.entries( data_fields ) ) { - if( Object.hasOwn( data, key ) ){ - filtered_data[ key ] = data[ key ]; - } - } - - if( Object.keys( filtered_data ).length === 0 ){ - throw "no data to add"; - } - - return filtered_data; -} - -async function createTemplate( req , res ) { - try{ - const companyId = req.context.companyId; - - const data = cleanUpData( req.body ); - data.company = companyId; - - const template = new LoadTemplatesModel( data ); - await template.save(); - - res.send( template ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -} - -async function updateTemplate( req , res ) { - try{ - const templateId = req.params.id; - const companyId = req.context.companyId; - - const data = cleanUpData( req.body ); - data.company = companyId; - - await LoadTemplatesModel.findByIdAndUpdate( templateId , data ); - - const template = await LoadTemplatesModel.findById( templateId ); - res.send( template ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -} - -async function getTemplate( req, res ){ - try{ - const templateId = req.params.id; - const template = await LoadTemplatesModel.findById( templateId ); - - if( ! template ){ - return res.status(400).send({ error : "invalid template id"}); - } - - res.send( template ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -} - -async function deleteTemplate( req, res ){ - try{ - const companyId = req.context.companyId; - const templateId = req.params.id; - await LoadTemplatesModel.findOneAndDelete({ - _id: templateId, - company : companyId - }); - res.send({ msg: "item removed successfully" }); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -} - -async function getAllTemplates( req, res ){ - try{ - const companyId = req.context.companyId; - const templateList = await LoadTemplatesModel.find( { - company : companyId - } ); - res.send( templateList ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -} - -module.exports = { - createTemplate, - getTemplate, - getAllTemplates, - updateTemplate, - deleteTemplate, -}; diff --git a/v1/src/apps/private/loads/routes.js b/v1/src/apps/private/loads/routes.js index 492b28b..038099a 100644 --- a/v1/src/apps/private/loads/routes.js +++ b/v1/src/apps/private/loads/routes.js @@ -3,7 +3,6 @@ const router = require('express').Router(); const services= require('./services.js'); router.get('/find', services.findList); -router.get('/driver', services.findDriverList); router.get('/calendar', services.findCalendarList); router.post('/new', services.postLoad); diff --git a/v1/src/apps/private/loads/services.js b/v1/src/apps/private/loads/services.js index 0512d1a..9d9b0f5 100644 --- a/v1/src/apps/private/loads/services.js +++ b/v1/src/apps/private/loads/services.js @@ -7,7 +7,6 @@ const Model = getModel('loads'); const CompanyModel = getModel('companies'); const ProposalsModel = getModel('proposals'); const branchesModel = getModel('branches'); -const companyGroupsModel = getModel('company_groups'); const carrier_projection = [ 'company_name', @@ -142,21 +141,7 @@ function getAndFilterList( query ){ return filter_list; } -async function getCompanyIdListFromGroups( companyId ){ - const privateGroups = await companyGroupsModel.find({ - allowedCompanies: { $in: [companyId] } - }); - - if( !privateGroups ){ - return null; - } - - const companiesIds = privateGroups.map((group) => group.owner); - - return companiesIds; -} - -async function findDriverLoads( query ){ +async function findLoads( query ){ const { $sort, company_name } = query; const { page, elements } = getPagination( query ); const andFilterList = getAndFilterList( query ) || []; @@ -199,65 +184,6 @@ async function findDriverLoads( query ){ }; } -async function findLoads( companyId, query ){ - const { $sort, company_name } = query; - const { page, elements } = getPagination( query ); - const andFilterList = getAndFilterList( query ) || []; - - const { privacy } = query; - const privacyVal = ( privacy && ( privacy >= 1 || privacy.toLowerCase() === 'true' ))? true: false; - - let filter; - - if( privacyVal ){ - const companiesIds = await getCompanyIdListFromGroups( companyId ) || []; - filter = { - company : { $in : companiesIds }, - privacy: true, - } - }else{ - filter = { - $or : [ - { privacy : false }, - { privacy : { $exists : false } } - ] - } - } - - if( company_name ){ - /* Populate list of company ids with match on the company_name */ - const company_list = await CompanyModel.find( { company_name }, [ "id" ] ); - const or_company_list = [] - company_list.forEach( (item) =>{ - or_company_list.push({"company" : item.id}); - }) - andFilterList.push({ - $or : or_company_list - }); - } - - if( andFilterList.length > 0 ){ - filter.$and = andFilterList; - } - - const { total , limit, skip, data } = await generic.getList( page , elements, filter, null, $sort ); - - const load_list = data; - - for(let i=0; i { const findList = async(req, res) => { try{ - const companyId = req.context.companyId; const query = req.query || {}; - const retVal = await findLoads( companyId, query ); + const retVal = await findLoads( query ); res.send( retVal ); }catch(error){ console.error( error ); @@ -365,17 +290,6 @@ const findList = async(req, res) => { } }; -const findDriverList = async ( req, res ) => { - try{ - const query = req.query || {}; - const retVal = await findDriverLoads( companyId, query ); - res.send( retVal ); - }catch(error){ - console.error( error ); - return res.status( 500 ).send({ error }); - } -} - const getById = async(req, res) => { try{ const elementId = req.params.id; @@ -442,7 +356,6 @@ function getDataToModify( data ){ notes : null, payment_term : null, terms_and_conditions : null, - privacy: null }; let filtered_data = {}; @@ -610,4 +523,4 @@ const deleteLoad = async(req, res) => { } }; -module.exports = { findCalendarList, findList, findDriverList, getById, patchLoad, postLoad, deleteLoad }; +module.exports = { findCalendarList, findList, getById, patchLoad, postLoad, deleteLoad }; diff --git a/v1/src/apps/public/public-companies/services.js b/v1/src/apps/public/public-companies/services.js index c15c9c3..c424ef6 100644 --- a/v1/src/apps/public/public-companies/services.js +++ b/v1/src/apps/public/public-companies/services.js @@ -46,14 +46,7 @@ function getAndFilterList( query ){ } async function getListByType( type , req ){ - const filter = { - company_type: type, - is_hidden: false, - $or : [ - { privacy : false }, - { privacy : { $exists : false } } - ] - }; + const filter = { "company_type" : type , "is_hidden" : false }; const select = [ "rfc", "company_name", @@ -65,8 +58,7 @@ async function getListByType( type , req ){ "membership", "categories", "truck_type", - "company_description", - "privacy" + "company_description" ]; const { elements } = getPagination( req.query ); const page = 0;// No pagination allowed to this endpoint diff --git a/v1/src/apps/public/public-loads/services.js b/v1/src/apps/public/public-loads/services.js index 2bd3290..f1cc0be 100644 --- a/v1/src/apps/public/public-loads/services.js +++ b/v1/src/apps/public/public-loads/services.js @@ -14,13 +14,7 @@ const generic = new GenericHandler( Model, null, populate_list ); const getList = async(req, res) => { try{ - const filter = { - status: "Published", - $or : [ - { privacy : false }, - { privacy : { $exists : false } } - ] - }; + const filter = { status : "Published" }; const select = [ "shipment_code", "categories", @@ -32,8 +26,7 @@ const getList = async(req, res) => { "est_loading_date", "est_unloading_date", "origin", - "destination", - "privacy" + "destination" ]; const { page , elements } = getPagination( req.query ); const retVal = await generic.getList(page , elements, filter, select ); diff --git a/v1/src/lib/Handlers/MailClient/StandAlone.handler.js b/v1/src/lib/Handlers/MailClient/StandAlone.handler.js index 3b93f58..79e9bee 100644 --- a/v1/src/lib/Handlers/MailClient/StandAlone.handler.js +++ b/v1/src/lib/Handlers/MailClient/StandAlone.handler.js @@ -53,7 +53,7 @@ async function sendMailTemplate( receiver, subject, html ){ async function StandAloneContactEmail( content ){ /** Send out the contact email to the default list of people */ - const receiver_list = ["eduardo.hernandez@etaviaporte.com","abel.mejia@etaviaporte.com"]; + const receiver_list = ["support@etaviaporte.com", "eduardo.hernandez@etaviaporte.com"]; const {name, email, message } = content; for(const receiver of receiver_list){ await transporter.sendMail({ diff --git a/v1/src/lib/Models/companies.model.js b/v1/src/lib/Models/companies.model.js index 222e1f9..76bbc17 100644 --- a/v1/src/lib/Models/companies.model.js +++ b/v1/src/lib/Models/companies.model.js @@ -59,7 +59,6 @@ const schema = new Schema({ lng: { type: String }, is_hidden: { type: Boolean, default: false }, - privacy: { type: Boolean, default: false }, /// Disables visibility on the directory, only enabled for private groups. createdAt: { type : Date, required : true, default : () => { return Date.now(); } } }); diff --git a/v1/src/lib/Models/company_groups.models.js b/v1/src/lib/Models/company_groups.models.js deleted file mode 100644 index d4296b1..0000000 --- a/v1/src/lib/Models/company_groups.models.js +++ /dev/null @@ -1,12 +0,0 @@ -const mongoose = require('mongoose'); -const { Schema } = mongoose; - -const schema = new Schema({ - owner: { type: Schema.Types.ObjectId, ref: 'companies', required: true }, /// Company owner of this list - list_name: { type: String }, /// Name of the list - allowedCompanies: [{ type: Schema.Types.ObjectId, ref: 'companies' }], /// Behaves as a tuple, not repeated elements - createdAt: { type : Date, required : true, default : () => { return Date.now(); } }, - updatedAt: { type : Date, required : true, default : () => { return Date.now(); } } -}); - -module.exports = mongoose.model( "companygroups", schema ); diff --git a/v1/src/lib/Models/index.js b/v1/src/lib/Models/index.js index 76345d8..5feef98 100644 --- a/v1/src/lib/Models/index.js +++ b/v1/src/lib/Models/index.js @@ -4,11 +4,9 @@ const branches = require('./branches.model.js'); const budgets = require('./budgets.model.js'); const cities = require('./cities.model.js'); const companies = require('./companies.model.js'); -const companygroups = require('./company_groups.models.js') const countries = require('./countries.model.js'); const load_attachments = require('./load-attachments.model.js'); const loads = require('./loads.model.js'); -const loadtemplates = require('./load_templates.model.js'); const mailer = require('./mailer.model.js'); const memberships = require('./memberships.model.js'); const meta_data = require('./meta-data.model.js'); @@ -36,16 +34,12 @@ function getModel( name ){ return cities; case 'companies': return companies; - case 'company_groups': - return companygroups; case 'countries': return countries; case 'load_attachments': return load_attachments; case 'loads': return loads; - case 'load_templates': - return loadtemplates; case 'mailer': return mailer; case 'memberships': diff --git a/v1/src/lib/Models/load_templates.model.js b/v1/src/lib/Models/load_templates.model.js deleted file mode 100644 index 400f353..0000000 --- a/v1/src/lib/Models/load_templates.model.js +++ /dev/null @@ -1,68 +0,0 @@ -const mongoose = require('mongoose'); -const { Schema } = mongoose; - -const address = new Schema({ - company_name: { type: String }, - - street_address1: { type: String }, - street_address2: { type: String }, - city: { type: String }, - state: { type: String }, - country: { type: String }, - zipcode: { type: String }, - - landmark: { type: String }, - - lat: { type: String }, - lng: { type: String }, -}); - - -const pointSchema = new Schema({ - type: { - type: String, - enum: ['Point'], - required: true - }, - coordinates: { - type: [Number], - required: true - } -}); - - -const schema = new Schema({ - company: { type: Schema.Types.ObjectId, ref: 'companies', required: true }, - template_name : { type: String }, - - alert_list: [{ type: String, lowercase: true }], - - origin_warehouse : { type: Schema.Types.ObjectId, ref: 'branches' }, - destination_warehouse : { type: Schema.Types.ObjectId, ref: 'branches' }, - - origin: address, - origin_geo: { - type: pointSchema, - }, - destination: address, - destination_geo: { - type: pointSchema, - }, - - categories: [{ type: Schema.Types.ObjectId, ref: 'productcategories' }], - product: { type: Schema.Types.ObjectId, ref: 'products' }, - - truck_type: { type: String }, - tyre_type: { type: String }, - weight: { type: Number }, - estimated_cost: { type: Number }, - - distance: { type: Number }, - actual_cost: { type: Number }, - notes: { type: String }, - - created_by: { type: Schema.Types.ObjectId, ref: 'users' }, // shipper - createdAt: { type : Date, required : true, default : () => { return Date.now(); } } -}); - -module.exports = mongoose.model( "loadtemplates", schema ); diff --git a/v1/src/lib/Models/loads.model.js b/v1/src/lib/Models/loads.model.js index 6c9fc02..988d3eb 100644 --- a/v1/src/lib/Models/loads.model.js +++ b/v1/src/lib/Models/loads.model.js @@ -90,9 +90,7 @@ const schema = new Schema({ payment_term: { type: String }, terms_and_conditions: { type: String }, - createdAt: { type : Date, required : true, default : () => { return Date.now(); } }, - - privacy: { type: Boolean, default: false }, /// Disables visibility on the directory, only enabled for private groups. + createdAt: { type : Date, required : true, default : () => { return Date.now(); } } }); module.exports = mongoose.model( "loads", schema ); From d06696fd0c86bf5f56d2f6d6ef03a338fb819f74 Mon Sep 17 00:00:00 2001 From: Josepablo Cruz Baas Date: Tue, 31 Mar 2026 22:47:31 +0000 Subject: [PATCH 3/3] Private Groups + Load-Templates Features --- .gitignore | 3 +- v1/README.md | 144 ++++++++++++++++-- v1/src/apps/private/companies/services.js | 80 +++++++--- v1/src/apps/private/groups/routes.js | 9 ++ v1/src/apps/private/groups/services.js | 116 ++++++++++++++ v1/src/apps/private/index.js | 17 +-- v1/src/apps/private/load-templates/routes.js | 11 ++ .../apps/private/load-templates/services.js | 137 +++++++++++++++++ v1/src/apps/private/loads/routes.js | 1 + v1/src/apps/private/loads/services.js | 93 ++++++++++- .../apps/public/public-companies/services.js | 12 +- v1/src/apps/public/public-loads/services.js | 11 +- .../Handlers/MailClient/StandAlone.handler.js | 2 +- v1/src/lib/Models/companies.model.js | 1 + v1/src/lib/Models/company_groups.models.js | 12 ++ v1/src/lib/Models/index.js | 6 + v1/src/lib/Models/load_templates.model.js | 68 +++++++++ v1/src/lib/Models/loads.model.js | 4 +- 18 files changed, 672 insertions(+), 55 deletions(-) create mode 100644 v1/src/apps/private/groups/routes.js create mode 100644 v1/src/apps/private/groups/services.js create mode 100644 v1/src/apps/private/load-templates/routes.js create mode 100644 v1/src/apps/private/load-templates/services.js create mode 100644 v1/src/lib/Models/company_groups.models.js create mode 100644 v1/src/lib/Models/load_templates.model.js diff --git a/.gitignore b/.gitignore index ffc83f1..20bd1d0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ **/migrate.js **/scripts/migrate/* v1/src/config/*.json -**/apiConfig.json \ No newline at end of file +**/apiConfig.json +ignore/* diff --git a/v1/README.md b/v1/README.md index 2e1b1cb..6576e3e 100644 --- a/v1/README.md +++ b/v1/README.md @@ -7,6 +7,11 @@ ETA Viaporte API - NodeJS v18 - Docker +# Audience + +This document is oriented to front-end developers integrating with the ETA API. +Use the Quick Start section first, then go to the endpoint section you need. + # Endpoints All endpoints that return a list of elements is paginable with the following queries: @@ -18,6 +23,71 @@ Example: - `/endpoint?page=2` : Get page 2 with default (10) elements per page. - `/endpoint?elements=50` : Get page 0 with 50 elements. +## Quick Start For Front-End Integration + +### Authentication + +Private endpoints require JWT authorization. + +1. Login with `POST /account/authorize`. +2. Store `accessToken` and `session_token`. +3. Send JWT in request headers: + +```http +Authorization: Bearer +``` + +4. When JWT expires, request a new token with `GET /account/authorize/:session_token`. + +### Account Lifecycle (Common Front-End Flow) + +1. Signup request: `POST /account/signup` returns `checksum`. +2. Signup confirmation: `PATCH /account/signup` with `otp` + `checksum`. +3. Login: `POST /account/authorize`. +4. Register company profile: `POST /account/register`. + +### Pagination + +Pagination is available on list endpoints using: + + - `elements`: page size + - `page`: page index (starts at 0) + +Examples: + + - `/loads?elements=20&page=0` + - `/loads?page=1` + - `/loads?elements=100` + +### Filters And Sorting + +Many `find` endpoints support dynamic filters and Mongo-style operators. + +Common patterns: + + - `field[$regex]=text` + - `field[$options]=i` + - `date[gte]=2026-01-01` + - `date[lte]=2026-01-31` + - `privacy=true` + - `$sort[createdAt]=-1` + +### Quick Endpoint Map + +Public resources: + + - Account and auth: `/account` + - Catalogs: `/countries`, `/cities`, `/states`, `/meta-data`, `/meta-groups`, `/product-categories` + - Public business data: `/public-companies`, `/public-loads`, `/public-vehicles` + - Contact: `/contact-email` + +Private resources: + + - Company and members: `/companies`, `/users` + - Operations: `/loads`, `/load-templates`, `/proposals`, `/vehicles`, `/branches`, `/budgets` + - Files and alerts: `/load-attachments`, `/notifications` + - Company private list: `/groups` + ## Public endpoints Read registered resources: @@ -39,19 +109,19 @@ All these endpoints support the following parameters (except for those with `pub - `/` : List registered resources with pagination. - `/:id` : Read specific resource identified by Id. - - `/find?regex=xxx` : List resources that matches with regex (support pagination). + - `/find?regex=xxx` : List resources that match a regex (supports pagination). ### /account This endpoint provides mechanisms to register, login, recover password and renew JWT. -The __Login__ and __Renew__ process will return 2 tokens, the `accessToken` (JWT) which is used in every further request to authorize access to private endpoints. And the 2nd is the `session_token` (renew token), which should be used after the expiration of the JWT, in order to generate another _JWT_ and _session token_ without the need of use the _email_ and _password__. +The __Login__ and __Renew__ processes return 2 tokens: `accessToken` (JWT), used for private endpoint authorization, and `session_token` (renew token), used after JWT expiration to generate a new JWT and session token without sending email and password again. The _session token_ expiration is typically 30 days after its generation. Every renewal replaces the token in the DB and the expiration is reset again to 30 days. #### POST /account/authorize -Login process, returns a JWT and Renew Token +Login process, returns a JWT and renew token Expects a body with the following data: ```{.json} @@ -94,7 +164,7 @@ Returns: #### POST /account/signup -Create a new user. This will trigger an email with the OTP (one time password) to verify the email. There is no expiration time, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. +Create a new user. This triggers an email with an OTP (one-time password) to verify the email. There is no expiration time, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. This will return a checksum string to be used in the confirmation process. @@ -116,7 +186,7 @@ Returns: #### PATCH /account/signup Confirms registration of new user. This will trigger a welcome email to the user. -There is no timeout to confirm the email, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. +There is no timeout to confirm the email, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. If the checksum matches but the user is already registered, then this request will be rejected. @@ -140,7 +210,7 @@ Returns: #### POST /account/recover -Reset password request. This will trigger an email with the OTP (one time password) to verify the email. There is no expiration time, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. +Reset password request. This triggers an email with an OTP (one-time password) to verify the email. There is no expiration time, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. This will return a checksum string to be used in the confirmation process. @@ -162,7 +232,7 @@ Returns: #### PATCH /account/recover Confirms the email to recover the password. -There is no timeout to confirm the email, but it is expected that the Fron End removes the checksum from the local storage after an expiration time defined in the Front End. +There is no timeout to confirm the email, but it is expected that the Front End removes the checksum from local storage after a client-defined timeout. Expects a body with the same data as the POST request, but adding the OTP received in the email, and the checksum generated by the POST request. Here is an example: @@ -206,6 +276,7 @@ Returns: ### GET /public-companies Get public fields from registered companies. +This endpoint only returns non-private companies (`privacy=false` or missing). - `GET /shipper`: List registered shippers that are not hidden only. - `GET /carrier`: List registered carriers that are not hidden only. @@ -213,6 +284,7 @@ Get public fields from registered companies. ### GET /public-loads Get public fields from registered loads. +This endpoint only returns published, non-private loads (`privacy=false` or missing). - `GET /`: List only loads with status Published. @@ -236,7 +308,15 @@ Get public fields from registered vehicles. The following list of endpoints requires a JWT. - `GET /loads`: List loads related to my company. + - `GET /load-templates/all`: List load templates from my company. - `GET /load-attachments`: List load attachments related to my company or load id. + - `GET /groups/private`: Get the private group for my company. + +Recommended header for all private endpoint calls: + +```http +Authorization: Bearer +``` ### /public-load-tracking @@ -343,21 +423,57 @@ This endpoint is part of /loads endpoint - `GET /own` : Get own company data. - `PATCH /own` : Update own company data. - - `GET /shipper` : Get list of shipper companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city. + - `GET /shipper` : Get list of shipper companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city, privacy. - $sort[ field ] : -1/1 ; Sort result by field name - - `GET /carrier` : Get list of carrier companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city + - `GET /carrier` : Get list of carrier companies with pagination using the following filters: company_type, company_name, truck_type, categories, company_state, company_city, privacy. - $sort[ field ] : -1/1 ; Sort result by field name + - `privacy=true`: return only private companies that explicitly granted visibility to my company through groups. + - `privacy=false` or unset: return only non-private companies. - `GET /users/:companyId` : Get the list of users within a company. - `GET /:id` : Get data from specific company. +### /groups + + - `GET /private` : Get the private group list from my company. If it does not exist, it will be created. + - `PATCH /private/:id` : Add a company (company id) to the private group list. + - `DELETE /private/:id` : Remove a company (company id) from the private group list. + ### /load-attachments - `POST /loading/:id` : Upload/Update a loading attachment. - - `POST /downloading/:id` : Upload/Update a download attachment + - `POST /downloading/:id` : Upload/Update an unloading attachment. - `GET /load/:id` : Get the list of attachment ids from load. - `GET /:id` : Get attachment file. - `GET /` : Get attachment list from company. +### /load-templates + + - `POST /new` : Create a new load template for my company. + - `GET /all` : Get all load templates from my company. + - `GET /:id` : Get a single load template by id. + - `PATCH /:id` : Update a load template by id. + - `DELETE /:id` : Delete a load template by id. + +Fields accepted for create/update: + + - template_name + - alert_list + - origin_warehouse + - destination_warehouse + - origin + - origin_geo + - destination + - destination_geo + - categories + - product + - truck_type + - tyre_type + - weight + - estimated_cost + - distance + - actual_cost + - notes + ### /loads - `GET /find` : Find a list of elements with any of the following fields: @@ -381,10 +497,12 @@ This endpoint is part of /loads endpoint - est_unloading_date[lte] : Date less than. - company_name[$regex] : Regex string to find company_name - company_name[$options] : Regex options from MongoDB filter description + - privacy : `true` to list private loads shared with my company through groups; `false` or unset to list non-private loads. - $sort[ field ] : -1/1 ; Sort result by field name - alert_list: List of emails to notify whenever there is a change to the object. + - `GET /driver` : Find a list of loads for driver views with the same filter and pagination behavior used in `/find`. Doesn't take privacy into account (already handled by driver assignation). - `GET /calendar` : Find a list of elements with any of the following fields: - - date[gte] : Date grater than. + - date[gte] : Date greater than. - date[lte] : Date less than. - load_status : string enumerator ['Published', 'Loading', 'Transit', 'Downloading', 'Delivered']. - global : 1 To return calendar of all company, or 0/undefined for personal calendar only. @@ -444,7 +562,7 @@ Work In Progress __This endpoint is only valid for carriers.__ - - `GET /find` : Find a list of elements (from the company it self only) with any of the following fields: + - `GET /find` : Find a list of elements (from the company itself only) with any of the following fields: - is_available, - categories, - active_load, @@ -490,7 +608,7 @@ __This endpoint is only valid for carriers.__ ### Public -The following endpoints doesn't require a JWT authorization +The following endpoints do not require JWT authorization. #### observers/account/ diff --git a/v1/src/apps/private/companies/services.js b/v1/src/apps/private/companies/services.js index 9c398ec..7c2b1fc 100644 --- a/v1/src/apps/private/companies/services.js +++ b/v1/src/apps/private/companies/services.js @@ -1,11 +1,12 @@ "use strict"; const { ROOT_PATH, MODELS_PATH, HANDLERS_PATH, LIB_PATH } = process.env; -const { getModel } = require( `${ROOT_PATH}/${MODELS_PATH}` ); +const { getModel } = require( '../../../lib/Models' ); const { GenericHandler } = require( '../../../lib/Handlers/Generic.handler.js' ); -const { getPagination } = require( `${ROOT_PATH}/${LIB_PATH}/Misc.js` ); +const { getPagination } = require( '../../../lib/Misc.js' ); const usersModel = getModel('users'); const companiesModel = getModel('companies'); +const companyGroupsModel = getModel('company_groups'); const branchesModel = getModel('branches'); const vehiclesModel = getModel('vehicles'); const loadsModel = getModel('loads'); @@ -46,21 +47,60 @@ function getAndFilterList( query ){ return filter_list; } -async function getListByType( type , req ){ - const filter = { "company_type" : type , "is_hidden" : false }; - const select = [ - "rfc", - "company_name", - "company_type", - "company_code", - "company_city", - "company_state", - "createdAt", - "membership", - "categories", - "truck_type", - "company_description" - ]; +const companySelectField = +[ + "rfc", + "company_name", + "company_type", + "company_code", + "company_city", + "company_state", + "createdAt", + "membership", + "categories", + "truck_type", + "company_description", + "privacy" +]; +async function getCompanyIdListFromGroups( companyId ){ + const privateGroups = await companyGroupsModel.find({ + allowedCompanies: { $in: [companyId] } + }); + + if( !privateGroups ){ + return null; + } + + const companiesIds = privateGroups.map((group) => group.owner); + + return companiesIds; +} + +async function getListByType( companyId, type , req ){ + const { privacy } = req.query; + const privacyVal = ( privacy && ( privacy >= 1 || privacy.toLowerCase() === 'true' ))? true: false; + + let filter; + if( privacyVal ){ + const companiesIds = await getCompanyIdListFromGroups( companyId ) || []; + filter = { + _id : { $in : companiesIds }, + company_type: type, + is_hidden: false, + privacy: true, + } + }else{ + filter = { + company_type: type, + is_hidden: false, + $or : [ + { privacy : false }, + { privacy : { $exists : false } } + ] + }; + } + + const select = companySelectField; const { $sort } = req.query; const { elements , page } = getPagination( req.query ); let query_elements; @@ -143,7 +183,8 @@ async function getCompanyById( req , res ) { async function getListShippers( req , res ) { try{ - const retVal = await getListByType( "Shipper" , req ); + const companyId = req.context.companyId; + const retVal = await getListByType( companyId, "Shipper" , req ); res.send( retVal ); }catch( error ){ console.error( error ); @@ -153,7 +194,8 @@ async function getListShippers( req , res ) { async function getListCarriers( req , res ) { try{ - const retVal = await getListByType( "Carrier" , req ); + const companyId = req.context.companyId; + const retVal = await getListByType( companyId, "Carrier" , req ); res.send( retVal ); }catch( error ){ console.error( error ); diff --git a/v1/src/apps/private/groups/routes.js b/v1/src/apps/private/groups/routes.js new file mode 100644 index 0000000..ae29577 --- /dev/null +++ b/v1/src/apps/private/groups/routes.js @@ -0,0 +1,9 @@ +'use strict'; +const router = require('express').Router(); +const services= require('./services.js'); + +router.get('/private', services.getPrivateGroup); +router.patch('/private/:id', services.addCompanyToGroup); +router.delete('/private/:id', services.removeCompanyFromGroup); + +module.exports = router; diff --git a/v1/src/apps/private/groups/services.js b/v1/src/apps/private/groups/services.js new file mode 100644 index 0000000..076e282 --- /dev/null +++ b/v1/src/apps/private/groups/services.js @@ -0,0 +1,116 @@ +"use strict"; +const { getModel } = require( '../../../lib/Models' ); +const { getPagination } = require( '../../../lib/Misc.js' ); + +const CompanyModel = getModel('companies'); +const CompanyGroupModel = getModel('company_groups'); + +const company_projection = 'company_name rfc categories products truck_type company_city company_state'; + +async function getOrCreatePrivateGroup( owner ) { + let result = await CompanyGroupModel + .findOne({ owner: owner }) + .populate({ + path: 'allowedCompanies', + select: company_projection, + populate: { + path: 'categories', + select: '-_id name' + } + }); + + if( result ){ + return result; + } + + result = new CompanyGroupModel({ + owner: owner, + list_name: "private" + }); + await result.save(); + + return await CompanyGroupModel + .findOne({ owner: owner }) + .populate({ + path: 'allowedCompanies', + select: company_projection, + populate: { + path: 'categories', + select: '-_id name' + } + }); +} + +async function getPrivateGroup( req , res ) { + try{ + const companyId = req.context.companyId; + const result = await getOrCreatePrivateGroup( companyId ); + return res.send( result ); + }catch( error ){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function addCompanyToGroup(req, res){ + try{ + const companyId = req.context.companyId; + const newEntryId = req.params.id; + + if( newEntryId === companyId ){ + return res.status(400).send({ error: "can't add yourself" }); + } + + const company = await CompanyModel.findById( newEntryId ); + if( !company ){ + return res.status(400).send({ error: "invalid entry id" }); + } + + let result = await getOrCreatePrivateGroup( companyId ); + + /// Add to the list only if it is not there already + if( ! result.allowedCompanies.some( entry => entry.id === newEntryId ) ){ + await CompanyGroupModel.findByIdAndUpdate(result.id, { + $addToSet: { allowedCompanies: company.id } + }); + } + + result = await getOrCreatePrivateGroup( companyId ); + return res.status(200).send( result ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function removeCompanyFromGroup(req, res){ + try{ + const companyId = req.context.companyId; + const newEntryId = req.params.id; + + if( newEntryId === companyId ){ + return res.status(400).send({ error: "you are not at the list" }); + } + + let result = await getOrCreatePrivateGroup( companyId ); + + /// If the item belongs to the list remove it + if( result.allowedCompanies.some( entry => entry.id === newEntryId ) ){ + await CompanyGroupModel.findByIdAndUpdate(result.id, { + $pull: { allowedCompanies: newEntryId } + }); + } + + result = await getOrCreatePrivateGroup( companyId ); + return res.status(200).send( result ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +module.exports = { + getPrivateGroup, + addCompanyToGroup, + removeCompanyFromGroup +}; diff --git a/v1/src/apps/private/index.js b/v1/src/apps/private/index.js index 1f62546..0632130 100644 --- a/v1/src/apps/private/index.js +++ b/v1/src/apps/private/index.js @@ -13,10 +13,12 @@ const companies = require('./companies/routes.js'); const loadAttachments = require('./load-attachments/routes.js'); const loads = require('./loads/routes.js'); const loads_driver = require('./loads_driver/routes.js'); +const loadTemplates = require('./load-templates/routes.js'); const proposals = require('./proposals/routes.js'); const users = require('./users/routes.js'); const vehicles = require('./vehicles/routes.js'); const notifications = require('./notifications/routes.js'); +const company_groups = require('./groups/routes.js'); router.use( jwtValidator.middleware ); router.use( context.middleware ); @@ -25,25 +27,14 @@ router.use('/account', account); router.use('/budgets', budgets); router.use('/branches', branches); router.use('/companies', companies); +router.use('/groups', company_groups); router.use('/load-attachments', loadAttachments ); router.use('/loads', loads); router.use('/loads_driver', loads_driver); +router.use('/load-templates', loadTemplates ); router.use('/notifications', notifications); router.use('/proposals', proposals); router.use('/users', users); router.use('/vehicles', vehicles); -/* -router.use('/orders', test); -router.use('/mailer', test); -router.use('/memberships', test); -router.use('/bootresolvers', test); -router.use('/news', test); -router.use('/branches', test); -router.use('/trackings', test); -router.use('/upload', test); -router.use('/calendars', test); -router.use('/dashboard', test); -*/ - module.exports = router; diff --git a/v1/src/apps/private/load-templates/routes.js b/v1/src/apps/private/load-templates/routes.js new file mode 100644 index 0000000..38a2605 --- /dev/null +++ b/v1/src/apps/private/load-templates/routes.js @@ -0,0 +1,11 @@ +'use strict'; +const router = require('express').Router(); +const services= require('./services.js'); + +router.post('/new', services.createTemplate); +router.get('/all', services.getAllTemplates); +router.get('/:id', services.getTemplate); +router.patch('/:id', services.updateTemplate); +router.delete('/:id', services.deleteTemplate); + +module.exports = router; diff --git a/v1/src/apps/private/load-templates/services.js b/v1/src/apps/private/load-templates/services.js new file mode 100644 index 0000000..56a5882 --- /dev/null +++ b/v1/src/apps/private/load-templates/services.js @@ -0,0 +1,137 @@ +"use strict"; +const { getModel } = require( '../../../lib/Models' ); +const { getPagination } = require( '../../../lib/Misc.js' ); + +const LoadTemplatesModel = getModel('load_templates'); + +function cleanUpData( data ){ + /** + * Take the only fields from model that + * should be modifiable by the client. + * The rest are populated on demand by the event handlers. + */ + let data_fields = { + template_name: null, + alert_list: null, + origin_warehouse: null, + destination_warehouse: null, + origin: null, + origin_geo: null, + destination: null, + destination_geo: null, + categories: null, + product: null, + truck_type: null, + tyre_type: null, + weight: null, + estimated_cost: null, + distance: null, + actual_cost: null, + notes: null, + }; + + let filtered_data = {}; + + if( Object.keys( data_fields ).length === 0 ){ + throw "no data to add"; + } + + for ( const [key, value] of Object.entries( data_fields ) ) { + if( Object.hasOwn( data, key ) ){ + filtered_data[ key ] = data[ key ]; + } + } + + if( Object.keys( filtered_data ).length === 0 ){ + throw "no data to add"; + } + + return filtered_data; +} + +async function createTemplate( req , res ) { + try{ + const companyId = req.context.companyId; + + const data = cleanUpData( req.body ); + data.company = companyId; + + const template = new LoadTemplatesModel( data ); + await template.save(); + + res.send( template ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function updateTemplate( req , res ) { + try{ + const templateId = req.params.id; + const companyId = req.context.companyId; + + const data = cleanUpData( req.body ); + data.company = companyId; + + await LoadTemplatesModel.findByIdAndUpdate( templateId , data ); + + const template = await LoadTemplatesModel.findById( templateId ); + res.send( template ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function getTemplate( req, res ){ + try{ + const templateId = req.params.id; + const template = await LoadTemplatesModel.findById( templateId ); + + if( ! template ){ + return res.status(400).send({ error : "invalid template id"}); + } + + res.send( template ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function deleteTemplate( req, res ){ + try{ + const companyId = req.context.companyId; + const templateId = req.params.id; + await LoadTemplatesModel.findOneAndDelete({ + _id: templateId, + company : companyId + }); + res.send({ msg: "item removed successfully" }); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +async function getAllTemplates( req, res ){ + try{ + const companyId = req.context.companyId; + const templateList = await LoadTemplatesModel.find( { + company : companyId + } ); + res.send( templateList ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + +module.exports = { + createTemplate, + getTemplate, + getAllTemplates, + updateTemplate, + deleteTemplate, +}; diff --git a/v1/src/apps/private/loads/routes.js b/v1/src/apps/private/loads/routes.js index 038099a..492b28b 100644 --- a/v1/src/apps/private/loads/routes.js +++ b/v1/src/apps/private/loads/routes.js @@ -3,6 +3,7 @@ const router = require('express').Router(); const services= require('./services.js'); router.get('/find', services.findList); +router.get('/driver', services.findDriverList); router.get('/calendar', services.findCalendarList); router.post('/new', services.postLoad); diff --git a/v1/src/apps/private/loads/services.js b/v1/src/apps/private/loads/services.js index 9d9b0f5..0512d1a 100644 --- a/v1/src/apps/private/loads/services.js +++ b/v1/src/apps/private/loads/services.js @@ -7,6 +7,7 @@ const Model = getModel('loads'); const CompanyModel = getModel('companies'); const ProposalsModel = getModel('proposals'); const branchesModel = getModel('branches'); +const companyGroupsModel = getModel('company_groups'); const carrier_projection = [ 'company_name', @@ -141,7 +142,21 @@ function getAndFilterList( query ){ return filter_list; } -async function findLoads( query ){ +async function getCompanyIdListFromGroups( companyId ){ + const privateGroups = await companyGroupsModel.find({ + allowedCompanies: { $in: [companyId] } + }); + + if( !privateGroups ){ + return null; + } + + const companiesIds = privateGroups.map((group) => group.owner); + + return companiesIds; +} + +async function findDriverLoads( query ){ const { $sort, company_name } = query; const { page, elements } = getPagination( query ); const andFilterList = getAndFilterList( query ) || []; @@ -184,6 +199,65 @@ async function findLoads( query ){ }; } +async function findLoads( companyId, query ){ + const { $sort, company_name } = query; + const { page, elements } = getPagination( query ); + const andFilterList = getAndFilterList( query ) || []; + + const { privacy } = query; + const privacyVal = ( privacy && ( privacy >= 1 || privacy.toLowerCase() === 'true' ))? true: false; + + let filter; + + if( privacyVal ){ + const companiesIds = await getCompanyIdListFromGroups( companyId ) || []; + filter = { + company : { $in : companiesIds }, + privacy: true, + } + }else{ + filter = { + $or : [ + { privacy : false }, + { privacy : { $exists : false } } + ] + } + } + + if( company_name ){ + /* Populate list of company ids with match on the company_name */ + const company_list = await CompanyModel.find( { company_name }, [ "id" ] ); + const or_company_list = [] + company_list.forEach( (item) =>{ + or_company_list.push({"company" : item.id}); + }) + andFilterList.push({ + $or : or_company_list + }); + } + + if( andFilterList.length > 0 ){ + filter.$and = andFilterList; + } + + const { total , limit, skip, data } = await generic.getList( page , elements, filter, null, $sort ); + + const load_list = data; + + for(let i=0; i { const findList = async(req, res) => { try{ + const companyId = req.context.companyId; const query = req.query || {}; - const retVal = await findLoads( query ); + const retVal = await findLoads( companyId, query ); res.send( retVal ); }catch(error){ console.error( error ); @@ -290,6 +365,17 @@ const findList = async(req, res) => { } }; +const findDriverList = async ( req, res ) => { + try{ + const query = req.query || {}; + const retVal = await findDriverLoads( companyId, query ); + res.send( retVal ); + }catch(error){ + console.error( error ); + return res.status( 500 ).send({ error }); + } +} + const getById = async(req, res) => { try{ const elementId = req.params.id; @@ -356,6 +442,7 @@ function getDataToModify( data ){ notes : null, payment_term : null, terms_and_conditions : null, + privacy: null }; let filtered_data = {}; @@ -523,4 +610,4 @@ const deleteLoad = async(req, res) => { } }; -module.exports = { findCalendarList, findList, getById, patchLoad, postLoad, deleteLoad }; +module.exports = { findCalendarList, findList, findDriverList, getById, patchLoad, postLoad, deleteLoad }; diff --git a/v1/src/apps/public/public-companies/services.js b/v1/src/apps/public/public-companies/services.js index c424ef6..c15c9c3 100644 --- a/v1/src/apps/public/public-companies/services.js +++ b/v1/src/apps/public/public-companies/services.js @@ -46,7 +46,14 @@ function getAndFilterList( query ){ } async function getListByType( type , req ){ - const filter = { "company_type" : type , "is_hidden" : false }; + const filter = { + company_type: type, + is_hidden: false, + $or : [ + { privacy : false }, + { privacy : { $exists : false } } + ] + }; const select = [ "rfc", "company_name", @@ -58,7 +65,8 @@ async function getListByType( type , req ){ "membership", "categories", "truck_type", - "company_description" + "company_description", + "privacy" ]; const { elements } = getPagination( req.query ); const page = 0;// No pagination allowed to this endpoint diff --git a/v1/src/apps/public/public-loads/services.js b/v1/src/apps/public/public-loads/services.js index f1cc0be..2bd3290 100644 --- a/v1/src/apps/public/public-loads/services.js +++ b/v1/src/apps/public/public-loads/services.js @@ -14,7 +14,13 @@ const generic = new GenericHandler( Model, null, populate_list ); const getList = async(req, res) => { try{ - const filter = { status : "Published" }; + const filter = { + status: "Published", + $or : [ + { privacy : false }, + { privacy : { $exists : false } } + ] + }; const select = [ "shipment_code", "categories", @@ -26,7 +32,8 @@ const getList = async(req, res) => { "est_loading_date", "est_unloading_date", "origin", - "destination" + "destination", + "privacy" ]; const { page , elements } = getPagination( req.query ); const retVal = await generic.getList(page , elements, filter, select ); diff --git a/v1/src/lib/Handlers/MailClient/StandAlone.handler.js b/v1/src/lib/Handlers/MailClient/StandAlone.handler.js index 79e9bee..3b93f58 100644 --- a/v1/src/lib/Handlers/MailClient/StandAlone.handler.js +++ b/v1/src/lib/Handlers/MailClient/StandAlone.handler.js @@ -53,7 +53,7 @@ async function sendMailTemplate( receiver, subject, html ){ async function StandAloneContactEmail( content ){ /** Send out the contact email to the default list of people */ - const receiver_list = ["support@etaviaporte.com", "eduardo.hernandez@etaviaporte.com"]; + const receiver_list = ["eduardo.hernandez@etaviaporte.com","abel.mejia@etaviaporte.com"]; const {name, email, message } = content; for(const receiver of receiver_list){ await transporter.sendMail({ diff --git a/v1/src/lib/Models/companies.model.js b/v1/src/lib/Models/companies.model.js index 76bbc17..222e1f9 100644 --- a/v1/src/lib/Models/companies.model.js +++ b/v1/src/lib/Models/companies.model.js @@ -59,6 +59,7 @@ const schema = new Schema({ lng: { type: String }, is_hidden: { type: Boolean, default: false }, + privacy: { type: Boolean, default: false }, /// Disables visibility on the directory, only enabled for private groups. createdAt: { type : Date, required : true, default : () => { return Date.now(); } } }); diff --git a/v1/src/lib/Models/company_groups.models.js b/v1/src/lib/Models/company_groups.models.js new file mode 100644 index 0000000..d4296b1 --- /dev/null +++ b/v1/src/lib/Models/company_groups.models.js @@ -0,0 +1,12 @@ +const mongoose = require('mongoose'); +const { Schema } = mongoose; + +const schema = new Schema({ + owner: { type: Schema.Types.ObjectId, ref: 'companies', required: true }, /// Company owner of this list + list_name: { type: String }, /// Name of the list + allowedCompanies: [{ type: Schema.Types.ObjectId, ref: 'companies' }], /// Behaves as a tuple, not repeated elements + createdAt: { type : Date, required : true, default : () => { return Date.now(); } }, + updatedAt: { type : Date, required : true, default : () => { return Date.now(); } } +}); + +module.exports = mongoose.model( "companygroups", schema ); diff --git a/v1/src/lib/Models/index.js b/v1/src/lib/Models/index.js index 5feef98..76345d8 100644 --- a/v1/src/lib/Models/index.js +++ b/v1/src/lib/Models/index.js @@ -4,9 +4,11 @@ const branches = require('./branches.model.js'); const budgets = require('./budgets.model.js'); const cities = require('./cities.model.js'); const companies = require('./companies.model.js'); +const companygroups = require('./company_groups.models.js') const countries = require('./countries.model.js'); const load_attachments = require('./load-attachments.model.js'); const loads = require('./loads.model.js'); +const loadtemplates = require('./load_templates.model.js'); const mailer = require('./mailer.model.js'); const memberships = require('./memberships.model.js'); const meta_data = require('./meta-data.model.js'); @@ -34,12 +36,16 @@ function getModel( name ){ return cities; case 'companies': return companies; + case 'company_groups': + return companygroups; case 'countries': return countries; case 'load_attachments': return load_attachments; case 'loads': return loads; + case 'load_templates': + return loadtemplates; case 'mailer': return mailer; case 'memberships': diff --git a/v1/src/lib/Models/load_templates.model.js b/v1/src/lib/Models/load_templates.model.js new file mode 100644 index 0000000..400f353 --- /dev/null +++ b/v1/src/lib/Models/load_templates.model.js @@ -0,0 +1,68 @@ +const mongoose = require('mongoose'); +const { Schema } = mongoose; + +const address = new Schema({ + company_name: { type: String }, + + street_address1: { type: String }, + street_address2: { type: String }, + city: { type: String }, + state: { type: String }, + country: { type: String }, + zipcode: { type: String }, + + landmark: { type: String }, + + lat: { type: String }, + lng: { type: String }, +}); + + +const pointSchema = new Schema({ + type: { + type: String, + enum: ['Point'], + required: true + }, + coordinates: { + type: [Number], + required: true + } +}); + + +const schema = new Schema({ + company: { type: Schema.Types.ObjectId, ref: 'companies', required: true }, + template_name : { type: String }, + + alert_list: [{ type: String, lowercase: true }], + + origin_warehouse : { type: Schema.Types.ObjectId, ref: 'branches' }, + destination_warehouse : { type: Schema.Types.ObjectId, ref: 'branches' }, + + origin: address, + origin_geo: { + type: pointSchema, + }, + destination: address, + destination_geo: { + type: pointSchema, + }, + + categories: [{ type: Schema.Types.ObjectId, ref: 'productcategories' }], + product: { type: Schema.Types.ObjectId, ref: 'products' }, + + truck_type: { type: String }, + tyre_type: { type: String }, + weight: { type: Number }, + estimated_cost: { type: Number }, + + distance: { type: Number }, + actual_cost: { type: Number }, + notes: { type: String }, + + created_by: { type: Schema.Types.ObjectId, ref: 'users' }, // shipper + createdAt: { type : Date, required : true, default : () => { return Date.now(); } } +}); + +module.exports = mongoose.model( "loadtemplates", schema ); diff --git a/v1/src/lib/Models/loads.model.js b/v1/src/lib/Models/loads.model.js index 988d3eb..6c9fc02 100644 --- a/v1/src/lib/Models/loads.model.js +++ b/v1/src/lib/Models/loads.model.js @@ -90,7 +90,9 @@ const schema = new Schema({ payment_term: { type: String }, terms_and_conditions: { type: String }, - createdAt: { type : Date, required : true, default : () => { return Date.now(); } } + createdAt: { type : Date, required : true, default : () => { return Date.now(); } }, + + privacy: { type: Boolean, default: false }, /// Disables visibility on the directory, only enabled for private groups. }); module.exports = mongoose.model( "loads", schema );