diff --git a/.env.model b/.env.model
index 5bae6ec850..b3dfc6ced4 100644
--- a/.env.model
+++ b/.env.model
@@ -54,8 +54,12 @@ NGINX_NETWORK_MODE=host|bridge
# Secret key to hash token
API_TOKEN_SECRET=*********
-# Secret for INSEE SIRENE API
-INSEE_SECRET=*********
+# ID et SECRET de l'application sur le portail https://portail-api.insee.fr/
+INSEE_CLIENT_ID=*********
+INSEE_CLIENT_SECRET=*********
+# Identifiant et mot de passe de https://portail-api.insee.fr/
+INSEE_USERNAME=*************
+INSEE_PASSWORD=*************
# Permet de court-circuiter l'API INSEE en cas de maintenance
INSEE_MAINTENANCE=true|false
@@ -87,6 +91,9 @@ VERIFIED_FOREIGN_TRANSPORTER_COMPANY_TEMPLATE_ID=*********
# Prefer minutes different from 0 to avoid traffic congestion
CRON_ONBOARDING_SCHEDULE=8 8 * * *
+# Daily cron job to cleanup isReturnFor tab
+CRON_CLEANUP_IS_RETURN_TAB_SCHEDULE=0 2 * * *
+
# SendInBlue configuration
SIB_BASE_URL=********
SIB_APIKEY=********
diff --git a/.github/workflows/add-ready-to-merge-label.yml b/.github/workflows/add-ready-to-merge-label.yml
index 756c38150e..4792813020 100644
--- a/.github/workflows/add-ready-to-merge-label.yml
+++ b/.github/workflows/add-ready-to-merge-label.yml
@@ -40,4 +40,10 @@ jobs:
owner: context.repo.owner,
repo: context.repo.repo,
name: "ready to merge"
+ }).catch(function(error) {
+ if (error.status === 404) {
+ return;
+ } else {
+ throw error;
+ }
})
\ No newline at end of file
diff --git a/.github/workflows/brgm.yml b/.github/workflows/brgm.yml
new file mode 100644
index 0000000000..f57f6c3148
--- /dev/null
+++ b/.github/workflows/brgm.yml
@@ -0,0 +1,24 @@
+name: Push to the BRGM GitLab repository
+
+on:
+ schedule:
+ - cron: '0 2 * * *' # Run every day at 2 AM
+ workflow_dispatch: # Allow manual trigger
+
+jobs:
+ push-to-gitlab:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout the repository
+ uses: actions/checkout@v4
+
+ - name: Set up Git
+ run: |
+ git config --global user.name "TD GH Action"
+ git config --global user.email "action@github.com"
+
+ - name: Add remote & push
+ run: |
+ git remote add brgm ${{ secrets.BRGM_GITLAB_URL }}
+ git push brgm --all
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 324e69d266..cd4ea983de 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -39,4 +39,5 @@ jobs:
TD_COMPANY_ELASTICSEARCH_URL: ${{ secrets.TD_COMPANY_ELASTICSEARCH_URL }}
TD_COMPANY_ELASTICSEARCH_CACERT: ${{ secrets.TD_COMPANY_ELASTICSEARCH_CACERT }}
OTEL_SDK_DISABLED: true
+ ELASTIC_SEARCH_URL: http://elasticsearch:9200
- run: npx nx affected -t build --parallel=3
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 3b009cc015..3fd919dad4 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -114,7 +114,11 @@ env:
QUEUE_MONITOR_TOKEN: token
DATABASE_URL: "postgresql://trackdechets:password@postgres:5432/e2e?schema=default$default"
API_TOKEN_SECRET: any_secret
- INSEE_SECRET: unset
+ INSEE_CLIENT_ID: unset
+ INSEE_CLIENT_SECRET: unset
+ INSEE_USERNAME: unset
+ INSEE_PASSWORD: unset
+
SESSION_SECRET: any_secret
SESSION_NAME: connect.sid
SESSION_COOKIE_HOST: trackdechets.local
@@ -211,6 +215,7 @@ env:
# Misc
CRON_ONBOARDING_SCHEDULE: false
+ CRON_CLEANUP_IS_RETURN_TAB_SCHEDULE: false
VERIFY_COMPANY: false
ALLOW_TEST_COMPANY: true
VITE_ALLOW_TEST_COMPANY: true
diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index cdde3ffc45..b459231b23 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -33,9 +33,10 @@ jobs:
sleep 5
- name: Run integration tests
run: |
- npx nx run back:test:integration -- --shard=${{ matrix.shard }}/${{ strategy.job-total }}
+ npx nx run-many --target=test --projects=back,cron --configuration=integration --parallel=1 -- --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
+ ALLOW_CLONING_BSDS: true
TD_COMPANY_ELASTICSEARCH_URL: ${{ secrets.TD_COMPANY_ELASTICSEARCH_URL }}
TD_COMPANY_ELASTICSEARCH_CACERT: ${{ secrets.TD_COMPANY_ELASTICSEARCH_CACERT }}
NODE_ENV: test
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d59b3eb1b4..3fe9285591 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -740,6 +740,12 @@ Pour palier à ce problème, il est possible de nourrir la base de donnée Prism
3.2 Dans le container `td-api`: `npx prisma db push --preview-feature` pour recréer les tables
4. Dans le container `td-api`: `npx prisma db seed --preview-feature` pour nourrir la base de données.
+### Ajouter un objet spécifique dans la base de données
+
+Au cas où il serait nécessaire d'ajouter un objet à la base de données, vous pouvez utiliser le script "object-creator". Pour celà, modifiez le fichier `libs/back/object-creator/src/objects.ts` en ajoutant des objets en respectant le format démontré en exemple.
+
+Vous pouvez ensuite utiliser `npx nx run object-creator:run` et si tout se passe bien, les objets seront créés dans la base de donnée spécifiée dans la variable d'environnement "DATABASE_URL".
+
### Ajouter une nouvelle icône
Les icônes utilisées dans l'application front viennent de https://streamlineicons.com/.
diff --git a/Changelog.md b/Changelog.md
index 03fc0fb303..e38bf6d0be 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -5,6 +5,43 @@ Les changements importants de Trackdéchets sont documentés dans ce fichier.
Le format est basé sur [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
et le projet suit un schéma de versionning inspiré de [Calendar Versioning](https://calver.org/).
+# [2024.10.1] 22/10/2024
+
+#### :rocket: Nouvelles fonctionnalités
+
+- Ajout d'Eco-organisme sur BSVHU [PR 3619](https://github.com/MTES-MCT/trackdechets/pull/3619)
+- Ajout des profils Négociant et Courtier sur BSVHU [PR 3645](https://github.com/MTES-MCT/trackdechets/pull/3645)
+- Ajout d'un moteur de recherche sur la documentation développeurs [PR 3622](https://github.com/MTES-MCT/trackdechets/pull/3622)
+- Ajout d'un nouvel onglet "Retours" pour les transporteurs [PR 3669](https://github.com/MTES-MCT/trackdechets/pull/3669)
+
+#### :nail_care: Améliorations
+
+- Changer la référence du cerfa lors de l'ajout d'une fiche d'intervention [PR 3616](https://github.com/MTES-MCT/trackdechets/pull/3616)
+- ETQ membre d'un établissement, je peux gérer mes préférences de notifications (demandes de rattachement, demandes de révisions, renouvellement code signature, etc) en lien avec cet établissement [PR 3634](https://github.com/MTES-MCT/trackdechets/pull/3634)
+- Amélioration du contenu de l'e-mail transactionnel envoyé au contact d'un établissement visé sur un bordereau en tant qu'émetteur [PR 3635](https://github.com/MTES-MCT/trackdechets/pull/3635)
+- Rendre les brouillons BSVHU non accessibles aux entreprises mentionnées sur le bordereau mais qui n'en sont pas les auteurs [PR 3677](https://github.com/MTES-MCT/trackdechets/pull/3677)
+
+#### :boom: Breaking changes
+
+- La Raison Sociale, le SIRET et l'Adresse de la destination sont scellés à la signature émetteur, sauf pour l'émetteur qui doit pouvoir le modifier jusqu'à la prochaine signature [PR 3628](https://github.com/MTES-MCT/trackdechets/pull/3628)
+
+- La complétion du champ identificationNumbers est obligatoire à la publication d'un VHU [PR 3628](https://github.com/MTES-MCT/trackdechets/pull/3628)
+
+#### :bug: Corrections de bugs
+
+- Documentation API Developers : Page Not Found, si on n'y accède pas via l'arborescence [PR 3621](https://github.com/MTES-MCT/trackdechets/pull/3621)
+- Ne pas apporter automatiquement de modification sur la liste des contenants lorsque je procède à une modification transporteur et que le BSFF est au statut SENT [PR 3615](https://github.com/MTES-MCT/trackdechets/pull/3615)
+- Correction du poids d'une annexe 1 après la révision du poids d'un BSD enfant [PR 3631](https://github.com/MTES-MCT/trackdechets/pull/3631)
+- Retrait du bouton de revision dans l'UI sur les DASRI regroupés [PR 3657](https://github.com/MTES-MCT/trackdechets/pull/3657)
+
+#### :house: Interne
+
+- Migration vers le nouveau portail API de l'INSEE [PR 3602](https://github.com/MTES-MCT/trackdechets/pull/3602)
+- Suppression du champ isRegistreNational [PR 3652](https://github.com/MTES-MCT/trackdechets/pull/3652)
+- ETQ utilisateur je peux cloner un BSD [PR 3637](https://github.com/MTES-MCT/trackdechets/pull/3637)
+- ETQ utilisateur je peux créer, révoquer et consulter mes demandes de délégation RNDTS [PR 3561](https://github.com/MTES-MCT/trackdechets/pull/3561) [PR 3588](https://github.com/MTES-MCT/trackdechets/pull/3588)
+- Ajout de la query controlBsds dédiée à la fiche établissment [PR 3694](https://github.com/MTES-MCT/trackdechets/pull/3694)
+
# [2024.9.1] 24/09/2024
#### :rocket: Nouvelles fonctionnalités
@@ -1019,7 +1056,7 @@ et le projet suit un schéma de versionning inspiré de [Calendar Versioning](ht
#### :house: Interne
- Meilleure gestion des ré-indexations de BSD sans interruption du service avec `npm run reindex-all-bsds-bulk` et parallélisation avec la job queue avec l'option `--useQueue` [PR1706](https://github.com/MTES-MCT/trackdechets/pull/1706).
-- Création de nouveau jobs `indexAllInBulk` et `indexChunk` pour la queue d'indexation, , création d'un groupe de workers de job spécifiques pour l'indexation `indexQueue` [PR1706](https://github.com/MTES-MCT/trackdechets/pull/1706).
+- Création de nouveau jobs `indexAllInBulk` et `indexChunk` pour la queue d'indexation, création d'un groupe de workers de job spécifiques pour l'indexation `indexQueue` [PR1706](https://github.com/MTES-MCT/trackdechets/pull/1706).
- Refonte d'un script de reindexation partielle avec interruption du service `npm run reindex-partial-in-place` avec une demande de confirmation dans la console [PR1706](https://github.com/MTES-MCT/trackdechets/pull/1706).
- Création d'un nouveau script pour vider de force une queue par son nom `npm run queue:obliterate` [PR1706](https://github.com/MTES-MCT/trackdechets/pull/1706).
- Suppression du script `bin/indexElasticSearch.ts` au profit des scripts `reindex*.ts` [PR1706](https://github.com/MTES-MCT/trackdechets/pull/1706).
diff --git a/apps/cron/src/commands/__tests__/onboarding.helpers.integration.ts b/apps/cron/src/commands/__tests__/onboarding.helpers.integration.ts
index 629a40b34c..7fda0353f3 100644
--- a/apps/cron/src/commands/__tests__/onboarding.helpers.integration.ts
+++ b/apps/cron/src/commands/__tests__/onboarding.helpers.integration.ts
@@ -2,6 +2,7 @@ import {
BsdaRevisionRequest,
BsdasriRevisionRequest,
MembershipRequestStatus,
+ UserNotification,
UserRole
} from "@prisma/client";
import { prisma } from "@td/prisma";
@@ -24,12 +25,12 @@ import { bsdaFactory } from "back/src/bsda/__tests__/factories";
import { bsdasriFactory } from "back/src/bsdasris/__tests__/factories";
import {
BsddRevisionRequestWithReadableId,
- addPendingApprovalsCompanyAdmins,
+ addPendingApprovalsCompanySubscribers,
getActiveUsersWithPendingMembershipRequests,
- getPendingBSDARevisionRequestsWithAdmins,
- getPendingBSDDRevisionRequestsWithAdmins,
- getPendingBSDASRIRevisionRequestsWithAdmins,
- getPendingMembershipRequestsAndAssociatedAdmins,
+ getPendingBSDARevisionRequestsWithSubscribers,
+ getPendingBSDDRevisionRequestsWithSubscribers,
+ getPendingBSDASRIRevisionRequestsWithSubscribers,
+ getPendingMembershipRequestsAndAssociatedSubscribers,
getRecentlyRegisteredProducers,
getRecentlyRegisteredProfesionals,
getRecentlyRegisteredUsersWithNoCompanyNorMembershipRequest
@@ -279,16 +280,40 @@ describe("getActiveUsersWithPendingMembershipRequests", () => {
});
});
-describe("getPendingMembershipRequestsAndAssociatedAdmins", () => {
+describe("getPendingMembershipRequestsAndAssociatedMailSubscribers ", () => {
afterEach(resetDatabase);
- it("should return pending membership requests created X days ago with all associated company admins", async () => {
+ it("should return pending membership requests created X days ago with all associated notification subscribers", async () => {
const user0 = await userFactory();
const user1 = await userFactory();
const user2 = await userFactory();
const user3 = await userFactory({ isActive: false });
- const companyAndAdmin0 = await userWithCompanyFactory("ADMIN");
+ const companyAndAdmin0 = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.MEMBERSHIP_REQUEST] }
+ );
+
+ // crée un second admin abonné aux notifications de demandes de rattachement
+ const admin01 = await userFactory();
+ await associateUserToCompany(
+ admin01.id,
+ companyAndAdmin0.company.orgId,
+ UserRole.ADMIN,
+ { notifications: [UserNotification.MEMBERSHIP_REQUEST] }
+ );
+
+ // crée un troisième admin qui n'est pas abonné aux notifications de demandes de rattachement
+ const admin02 = await userFactory();
+ await associateUserToCompany(
+ admin02.id,
+ companyAndAdmin0.company.orgId,
+ UserRole.ADMIN,
+ { notifications: [] }
+ );
+
const companyAndAdmin1 = await userWithCompanyFactory("ADMIN");
const companyAndAdmin2 = await userWithCompanyFactory("ADMIN");
const companyAndAdmin3 = await userWithCompanyFactory("ADMIN");
@@ -321,87 +346,36 @@ describe("getPendingMembershipRequestsAndAssociatedAdmins", () => {
status: MembershipRequestStatus.PENDING
});
- const requests = await getPendingMembershipRequestsAndAssociatedAdmins(3);
-
- expect(requests.length).toEqual(1);
-
- const expectedResult = [
- {
- requestId: request0.id,
- email: user0.email,
- orgId: companyAndAdmin0.company.orgId,
- adminIds: [companyAndAdmin0.user.id]
- }
- ];
-
- const actualResult = requests.map(r => ({
- requestId: r.id,
- email: r.user.email,
- orgId: r.company.orgId,
- adminIds: r.company.companyAssociations.map(a => a.user.id).sort()
- }));
-
- expect(expectedResult).toEqual(actualResult);
- });
-
- it("should return ALL active admins from request's company", async () => {
- const user0 = await userFactory();
-
- // Should be returned
- const companyAndAdmin0 = await userWithCompanyFactory("ADMIN");
-
- // Should be returned
- const admin1 = await userFactory({
- companyAssociations: {
- create: {
- company: { connect: { id: companyAndAdmin0.company.id } },
- role: "ADMIN"
- }
- }
- });
-
- // Should not be returned, because unactive
- await userFactory({
- isActive: false,
- companyAssociations: {
- create: {
- company: { connect: { id: companyAndAdmin0.company.id } },
- role: "ADMIN"
- }
- }
- });
-
- // Should be returned, along with both admins
- const request0 = await createMembershipRequest(
- user0,
- companyAndAdmin0.company,
- {
- createdAt: THREE_DAYS_AGO,
- status: MembershipRequestStatus.PENDING
- }
+ const requests = await getPendingMembershipRequestsAndAssociatedSubscribers(
+ 3
);
- const requests = await getPendingMembershipRequestsAndAssociatedAdmins(3);
-
expect(requests.length).toEqual(1);
- const expectedResult = [
- {
- requestId: request0.id,
- email: user0.email,
- orgId: companyAndAdmin0.company.orgId,
- adminIds: [companyAndAdmin0.user.id, admin1.id].sort()
- }
- ];
-
- const actualResult = requests.map(r => ({
- requestId: r.id,
- email: r.user.email,
- orgId: r.company.orgId,
- adminIds: r.company.companyAssociations.map(a => a.user.id).sort()
- }));
-
- expect(expectedResult).toEqual(actualResult);
+ expect(requests).toEqual([
+ expect.objectContaining({
+ id: request0.id,
+ user: expect.objectContaining({ email: user0.email }),
+ company: expect.objectContaining({
+ orgId: companyAndAdmin0.company.orgId,
+ companyAssociations: [
+ expect.objectContaining({
+ user: expect.objectContaining({
+ email: companyAndAdmin0.user.email
+ })
+ }),
+ expect.objectContaining({
+ user: expect.objectContaining({
+ email: admin01.email
+ })
+ })
+ // admin02 ne doit pas être présent ici car
+ // il n'est pas abonné aux notifications de
+ // demandes de rattachement
+ ]
+ })
+ })
+ ]);
});
it("should return all requests from a user, even if targeting different companies", async () => {
@@ -433,39 +407,34 @@ describe("getPendingMembershipRequestsAndAssociatedAdmins", () => {
}
);
- const requests = await getPendingMembershipRequestsAndAssociatedAdmins(3);
+ const requests = await getPendingMembershipRequestsAndAssociatedSubscribers(
+ 3
+ );
expect(requests.length).toEqual(2);
- const expectedResult = [
- {
- requestId: request0.id,
- email: user0.email,
- orgId: companyAndAdmin0.company.orgId,
- adminIds: [companyAndAdmin0.user.id]
- },
- {
- requestId: request1.id,
- email: user0.email,
- orgId: companyAndAdmin1.company.orgId,
- adminIds: [companyAndAdmin1.user.id]
- }
- ].sort((a, b) => a.requestId.localeCompare(b.requestId));
-
- const actualResult = requests
- .map(r => ({
- requestId: r.id,
- email: r.user.email,
- orgId: r.company.orgId,
- adminIds: r.company.companyAssociations.map(a => a.user.id).sort()
- }))
- .sort((a, b) => a.requestId.localeCompare(b.requestId));
-
- expect(expectedResult).toEqual(actualResult);
+ expect(
+ requests.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
+ ).toEqual([
+ expect.objectContaining({
+ id: request0.id,
+ company: expect.objectContaining({
+ orgId: companyAndAdmin0.company.orgId
+ }),
+ user: expect.objectContaining({ email: user0.email })
+ }),
+ expect.objectContaining({
+ id: request1.id,
+ company: expect.objectContaining({
+ orgId: companyAndAdmin1.company.orgId
+ }),
+ user: expect.objectContaining({ email: user0.email })
+ })
+ ]);
});
});
-describe("getPendingBSDARevisionRequestsWithAdmins", () => {
+describe("getPendingBSDARevisionRequestsWithSubscribers", () => {
afterEach(resetDatabase);
it("should return empty array if DB is empty", async () => {
@@ -473,15 +442,20 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
// When
const pendingBSDARevisionRequest =
- await getPendingBSDARevisionRequestsWithAdmins(2);
+ await getPendingBSDARevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDARevisionRequest.length).toEqual(0);
});
- it("should return pending request and company admins", async () => {
+ it("should return pending request and subscribers to REVISION_REQUEST notifications", async () => {
// Given
- const { user, company } = await userWithCompanyFactory("ADMIN");
+ const { user, company } = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
+ );
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -505,7 +479,7 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
// When
const pendingBSDARevisionRequest =
- await getPendingBSDARevisionRequestsWithAdmins(2);
+ await getPendingBSDARevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDARevisionRequest.length).toEqual(1);
@@ -517,13 +491,15 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
expect(pendingBSDARevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
- expect(pendingBSDARevisionRequest[0].approvals[0].admins.length).toEqual(1);
- expect(pendingBSDARevisionRequest[0].approvals[0].admins[0].id).toEqual(
- user.id
- );
+ expect(
+ pendingBSDARevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(
+ pendingBSDARevisionRequest[0].approvals[0].subscribers[0].id
+ ).toEqual(user.id);
});
- it("should not return pending request and company admins NOT xDaysAgo", async () => {
+ it("should not return pending request and subscribers NOT xDaysAgo", async () => {
// Given
const { company } = await userWithCompanyFactory("ADMIN");
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
@@ -568,7 +544,7 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
// When
const pendingBSDARevisionRequest =
- await getPendingBSDARevisionRequestsWithAdmins(2);
+ await getPendingBSDARevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDARevisionRequest.length).toEqual(0);
@@ -602,7 +578,7 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
// When
const pendingBSDARevisionRequest =
- await getPendingBSDARevisionRequestsWithAdmins(2);
+ await getPendingBSDARevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDARevisionRequest.length).toEqual(0);
@@ -612,7 +588,12 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
// Given
// Request 1
- const { user, company } = await userWithCompanyFactory("ADMIN");
+ const { user, company } = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
+ );
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -636,7 +617,10 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
// Request 2
const { user: user2, company: company2 } = await userWithCompanyFactory(
- "ADMIN"
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
);
const { company: companyOfSomeoneElse2 } = await userWithCompanyFactory(
"ADMIN"
@@ -661,7 +645,7 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
// When
const pendingBSDARevisionRequest =
- await getPendingBSDARevisionRequestsWithAdmins(2);
+ await getPendingBSDARevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDARevisionRequest.length).toEqual(2);
@@ -671,10 +655,12 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
(pendingBSDARevisionRequest[0] as BsdaRevisionRequest).bsdaId
).toEqual(bsda.id);
expect(pendingBSDARevisionRequest[0].approvals.length).toEqual(1);
- expect(pendingBSDARevisionRequest[0].approvals[0].admins.length).toEqual(1);
- expect(pendingBSDARevisionRequest[0].approvals[0].admins[0].id).toEqual(
- user.id
- );
+ expect(
+ pendingBSDARevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(
+ pendingBSDARevisionRequest[0].approvals[0].subscribers[0].id
+ ).toEqual(user.id);
expect(pendingBSDARevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
@@ -684,20 +670,30 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
(pendingBSDARevisionRequest[1] as BsdaRevisionRequest).bsdaId
).toEqual(bsda2.id);
expect(pendingBSDARevisionRequest[1].approvals.length).toEqual(1);
- expect(pendingBSDARevisionRequest[1].approvals[0].admins.length).toEqual(1);
- expect(pendingBSDARevisionRequest[1].approvals[0].admins[0].id).toEqual(
- user2.id
- );
+ expect(
+ pendingBSDARevisionRequest[1].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(
+ pendingBSDARevisionRequest[1].approvals[0].subscribers[0].id
+ ).toEqual(user2.id);
expect(pendingBSDARevisionRequest[1].approvals[0].company.id).toEqual(
company2.id
);
});
- it("should return all admins from pending companies", async () => {
+ it("should return all subscribers from pending companies", async () => {
// Given
- const { user, company } = await userWithCompanyFactory("ADMIN");
+ const { user, company } = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
+ );
const user2 = await userFactory();
- await associateUserToCompany(user2.id, company.orgId, "ADMIN");
+ await associateUserToCompany(user2.id, company.orgId, "ADMIN", {
+ notifications: [UserNotification.REVISION_REQUEST]
+ });
+
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -721,7 +717,7 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
// When
const pendingBSDARevisionRequest =
- await getPendingBSDARevisionRequestsWithAdmins(2);
+ await getPendingBSDARevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDARevisionRequest.length).toEqual(1);
@@ -733,20 +729,24 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
expect(pendingBSDARevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
- expect(pendingBSDARevisionRequest[0].approvals[0].admins.length).toEqual(2);
+ expect(
+ pendingBSDARevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(2);
- const adminIds = pendingBSDARevisionRequest[0].approvals[0].admins.map(
+ const adminIds = pendingBSDARevisionRequest[0].approvals[0].subscribers.map(
a => a.id
);
expect(adminIds.includes(user.id)).toBe(true);
expect(adminIds.includes(user2.id)).toBe(true);
});
- it("should not return non-admins from pending companies", async () => {
+ it("should not return non subscribers from pending companies", async () => {
// Given
const { user, company } = await userWithCompanyFactory("ADMIN");
const user2 = await userFactory();
- await associateUserToCompany(user2.id, company.orgId, "MEMBER");
+ await associateUserToCompany(user2.id, company.orgId, "ADMIN", {
+ notifications: []
+ });
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -770,7 +770,7 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
// When
const pendingBSDARevisionRequest =
- await getPendingBSDARevisionRequestsWithAdmins(2);
+ await getPendingBSDARevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDARevisionRequest.length).toEqual(1);
@@ -782,14 +782,16 @@ describe("getPendingBSDARevisionRequestsWithAdmins", () => {
expect(pendingBSDARevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
- expect(pendingBSDARevisionRequest[0].approvals[0].admins.length).toEqual(1);
- expect(pendingBSDARevisionRequest[0].approvals[0].admins[0].id).toEqual(
- user.id
- );
+ expect(
+ pendingBSDARevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(
+ pendingBSDARevisionRequest[0].approvals[0].subscribers[0].id
+ ).toEqual(user.id);
});
});
-describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
+describe("getPendingBSDDRevisionRequestsWithSubscribers", () => {
afterEach(resetDatabase);
it("should return empty array if DB is empty", async () => {
@@ -797,15 +799,20 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
// When
const pendingBSDRevisionRequest =
- await getPendingBSDDRevisionRequestsWithAdmins(2);
+ await getPendingBSDDRevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDRevisionRequest.length).toEqual(0);
});
- it("should return pending request and company admins", async () => {
+ it("should return pending request and company subscribers to REVISION_REQUEST notification", async () => {
// Given
- const { user, company } = await userWithCompanyFactory("ADMIN");
+ const { user, company } = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
+ );
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -827,7 +834,7 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
// When
const pendingBSDRevisionRequest =
- await getPendingBSDDRevisionRequestsWithAdmins(2);
+ await getPendingBSDDRevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDRevisionRequest.length).toEqual(1);
@@ -837,8 +844,10 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
.readableId
).toEqual(bsdd.readableId);
expect(pendingBSDRevisionRequest[0].approvals.length).toEqual(1);
- expect(pendingBSDRevisionRequest[0].approvals[0].admins.length).toEqual(1);
- expect(pendingBSDRevisionRequest[0].approvals[0].admins[0].id).toEqual(
+ expect(
+ pendingBSDRevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(pendingBSDRevisionRequest[0].approvals[0].subscribers[0].id).toEqual(
user.id
);
expect(pendingBSDRevisionRequest[0].approvals[0].company.id).toEqual(
@@ -846,7 +855,7 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
);
});
- it("should not return pending request and company admins NOT xDaysAgo", async () => {
+ it("should not return pending request and subscribers NOT xDaysAgo", async () => {
// Given
const { user, company } = await userWithCompanyFactory("ADMIN");
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
@@ -887,7 +896,7 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
// When
const pendingBSDRevisionRequest =
- await getPendingBSDDRevisionRequestsWithAdmins(2);
+ await getPendingBSDDRevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDRevisionRequest.length).toEqual(0);
@@ -919,17 +928,22 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
// When
const pendingBSDRevisionRequest =
- await getPendingBSDDRevisionRequestsWithAdmins(2);
+ await getPendingBSDDRevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDRevisionRequest.length).toEqual(0);
});
- it("should return multiple pending request and company admins", async () => {
+ it("should return multiple pending request and subsrribers", async () => {
// Given
// Request 1
- const { user, company } = await userWithCompanyFactory("ADMIN");
+ const { user, company } = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
+ );
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -951,7 +965,10 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
// Request 2
const { user: user2, company: company2 } = await userWithCompanyFactory(
- "ADMIN"
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
);
const { company: companyOfSomeoneElse2 } = await userWithCompanyFactory(
"ADMIN"
@@ -974,7 +991,7 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
// When
const pendingBSDRevisionRequest =
- await getPendingBSDDRevisionRequestsWithAdmins(2);
+ await getPendingBSDDRevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDRevisionRequest.length).toEqual(2);
@@ -985,8 +1002,10 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
.readableId
).toEqual(bsdd.readableId);
expect(pendingBSDRevisionRequest[0].approvals.length).toEqual(1);
- expect(pendingBSDRevisionRequest[0].approvals[0].admins.length).toEqual(1);
- expect(pendingBSDRevisionRequest[0].approvals[0].admins[0].id).toEqual(
+ expect(
+ pendingBSDRevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(pendingBSDRevisionRequest[0].approvals[0].subscribers[0].id).toEqual(
user.id
);
expect(pendingBSDRevisionRequest[0].approvals[0].company.id).toEqual(
@@ -999,8 +1018,10 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
.readableId
).toEqual(bsdd2.readableId);
expect(pendingBSDRevisionRequest[1].approvals.length).toEqual(1);
- expect(pendingBSDRevisionRequest[1].approvals[0].admins.length).toEqual(1);
- expect(pendingBSDRevisionRequest[1].approvals[0].admins[0].id).toEqual(
+ expect(
+ pendingBSDRevisionRequest[1].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(pendingBSDRevisionRequest[1].approvals[0].subscribers[0].id).toEqual(
user2.id
);
expect(pendingBSDRevisionRequest[1].approvals[0].company.id).toEqual(
@@ -1008,11 +1029,18 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
);
});
- it("should return all admins from pending companies", async () => {
+ it("should return all subscribers from pending companies", async () => {
// Given
- const { user, company } = await userWithCompanyFactory("ADMIN");
+ const { user, company } = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
+ );
const user2 = await userFactory();
- await associateUserToCompany(user2.id, company.orgId, "ADMIN");
+ await associateUserToCompany(user2.id, company.orgId, "ADMIN", {
+ notifications: [UserNotification.REVISION_REQUEST]
+ });
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -1034,7 +1062,7 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
// When
const pendingBSDRevisionRequest =
- await getPendingBSDDRevisionRequestsWithAdmins(2);
+ await getPendingBSDDRevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDRevisionRequest.length).toEqual(1);
@@ -1047,20 +1075,24 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
expect(pendingBSDRevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
- expect(pendingBSDRevisionRequest[0].approvals[0].admins.length).toEqual(2);
+ expect(
+ pendingBSDRevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(2);
- const adminIds = pendingBSDRevisionRequest[0].approvals[0].admins.map(
+ const adminIds = pendingBSDRevisionRequest[0].approvals[0].subscribers.map(
a => a.id
);
expect(adminIds.includes(user.id)).toBe(true);
expect(adminIds.includes(user2.id)).toBe(true);
});
- it("should not return non-admins from pending companies", async () => {
+ it("should not return non-subscribers from pending companies", async () => {
// Given
const { user, company } = await userWithCompanyFactory("ADMIN");
const user2 = await userFactory();
- await associateUserToCompany(user2.id, company.orgId, "MEMBER");
+ await associateUserToCompany(user2.id, company.orgId, "ADMIN", {
+ notifications: []
+ });
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -1082,7 +1114,7 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
// When
const pendingBSDRevisionRequest =
- await getPendingBSDDRevisionRequestsWithAdmins(2);
+ await getPendingBSDDRevisionRequestsWithSubscribers(2);
// Then
expect(pendingBSDRevisionRequest.length).toEqual(1);
@@ -1095,24 +1127,26 @@ describe("getPendingBSDDRevisionRequestsWithAdmins", () => {
expect(pendingBSDRevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
- expect(pendingBSDRevisionRequest[0].approvals[0].admins.length).toEqual(1);
- expect(pendingBSDRevisionRequest[0].approvals[0].admins[0].id).toEqual(
+ expect(
+ pendingBSDRevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(pendingBSDRevisionRequest[0].approvals[0].subscribers[0].id).toEqual(
user.id
);
});
});
-describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
+describe("getPendingBSDARIRevisionRequestsWithSubscribers", () => {
afterEach(resetDatabase);
it("should return empty array if DB is empty", async () => {
const pendingBSDASRIRevisionRequest =
- await getPendingBSDASRIRevisionRequestsWithAdmins(2);
+ await getPendingBSDASRIRevisionRequestsWithSubscribers(2);
expect(pendingBSDASRIRevisionRequest.length).toEqual(0);
});
- it("should return pending request and company admins", async () => {
+ it("should return pending request and company subscribers to notification REVISION_REQUEST", async () => {
const { user, company } = await userWithCompanyFactory("ADMIN");
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
@@ -1136,7 +1170,7 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
});
const pendingBSDARIRevisionRequest =
- await getPendingBSDASRIRevisionRequestsWithAdmins(2);
+ await getPendingBSDASRIRevisionRequestsWithSubscribers(2);
expect(pendingBSDARIRevisionRequest.length).toEqual(1);
expect(pendingBSDARIRevisionRequest[0].id).toEqual(request.id);
@@ -1147,15 +1181,15 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
expect(pendingBSDARIRevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
- expect(pendingBSDARIRevisionRequest[0].approvals[0].admins.length).toEqual(
- 1
- );
- expect(pendingBSDARIRevisionRequest[0].approvals[0].admins[0].id).toEqual(
- user.id
- );
+ expect(
+ pendingBSDARIRevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(
+ pendingBSDARIRevisionRequest[0].approvals[0].subscribers[0].id
+ ).toEqual(user.id);
});
- it("should not return pending request and company admins NOT xDaysAgo", async () => {
+ it("should not return pending request and company subscribers NOT xDaysAgo", async () => {
const { company } = await userWithCompanyFactory("ADMIN");
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
@@ -1198,7 +1232,7 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
});
const pendingBSDASRIRevisionRequest =
- await getPendingBSDASRIRevisionRequestsWithAdmins(2);
+ await getPendingBSDASRIRevisionRequestsWithSubscribers(2);
expect(pendingBSDASRIRevisionRequest.length).toEqual(0);
});
@@ -1229,14 +1263,19 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
});
const pendingBSDASRIRevisionRequest =
- await getPendingBSDASRIRevisionRequestsWithAdmins(2);
+ await getPendingBSDASRIRevisionRequestsWithSubscribers(2);
expect(pendingBSDASRIRevisionRequest.length).toEqual(0);
});
- it("should return multiple pending request and company admins", async () => {
+ it("should return multiple pending request and company subscribers", async () => {
// Request 1
- const { user, company } = await userWithCompanyFactory("ADMIN");
+ const { user, company } = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
+ );
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -1260,7 +1299,10 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
// Request 2
const { user: user2, company: company2 } = await userWithCompanyFactory(
- "ADMIN"
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
);
const { company: companyOfSomeoneElse2 } = await userWithCompanyFactory(
"ADMIN"
@@ -1284,7 +1326,7 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
});
const pendingBSDASRIRevisionRequest =
- await getPendingBSDASRIRevisionRequestsWithAdmins(2);
+ await getPendingBSDASRIRevisionRequestsWithSubscribers(2);
expect(pendingBSDASRIRevisionRequest.length).toEqual(2);
@@ -1293,12 +1335,12 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
(pendingBSDASRIRevisionRequest[0] as BsdasriRevisionRequest).bsdasriId
).toEqual(bsdasri.id);
expect(pendingBSDASRIRevisionRequest[0].approvals.length).toEqual(1);
- expect(pendingBSDASRIRevisionRequest[0].approvals[0].admins.length).toEqual(
- 1
- );
- expect(pendingBSDASRIRevisionRequest[0].approvals[0].admins[0].id).toEqual(
- user.id
- );
+ expect(
+ pendingBSDASRIRevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(
+ pendingBSDASRIRevisionRequest[0].approvals[0].subscribers[0].id
+ ).toEqual(user.id);
expect(pendingBSDASRIRevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
@@ -1308,21 +1350,28 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
(pendingBSDASRIRevisionRequest[1] as BsdasriRevisionRequest).bsdasriId
).toEqual(bsdasri2.id);
expect(pendingBSDASRIRevisionRequest[1].approvals.length).toEqual(1);
- expect(pendingBSDASRIRevisionRequest[1].approvals[0].admins.length).toEqual(
- 1
- );
- expect(pendingBSDASRIRevisionRequest[1].approvals[0].admins[0].id).toEqual(
- user2.id
- );
+ expect(
+ pendingBSDASRIRevisionRequest[1].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(
+ pendingBSDASRIRevisionRequest[1].approvals[0].subscribers[0].id
+ ).toEqual(user2.id);
expect(pendingBSDASRIRevisionRequest[1].approvals[0].company.id).toEqual(
company2.id
);
});
- it("should return all admins from pending companies", async () => {
- const { user, company } = await userWithCompanyFactory("ADMIN");
+ it("should return all subscribers from pending companies", async () => {
+ const { user, company } = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
+ );
const user2 = await userFactory();
- await associateUserToCompany(user2.id, company.orgId, "ADMIN");
+ await associateUserToCompany(user2.id, company.orgId, "ADMIN", {
+ notifications: [UserNotification.REVISION_REQUEST]
+ });
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -1345,7 +1394,7 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
});
const pendingBSDASRIevisionRequest =
- await getPendingBSDASRIRevisionRequestsWithAdmins(2);
+ await getPendingBSDASRIRevisionRequestsWithSubscribers(2);
expect(pendingBSDASRIevisionRequest.length).toEqual(1);
expect(pendingBSDASRIevisionRequest[0].approvals.length).toEqual(1);
@@ -1356,21 +1405,22 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
expect(pendingBSDASRIevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
- expect(pendingBSDASRIevisionRequest[0].approvals[0].admins.length).toEqual(
- 2
- );
+ expect(
+ pendingBSDASRIevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(2);
- const adminIds = pendingBSDASRIevisionRequest[0].approvals[0].admins.map(
- a => a.id
- );
+ const adminIds =
+ pendingBSDASRIevisionRequest[0].approvals[0].subscribers.map(a => a.id);
expect(adminIds.includes(user.id)).toBe(true);
expect(adminIds.includes(user2.id)).toBe(true);
});
- it("should not return non-admins from pending companies", async () => {
+ it("should not return non-subscribers from pending companies", async () => {
const { user, company } = await userWithCompanyFactory("ADMIN");
const user2 = await userFactory();
- await associateUserToCompany(user2.id, company.orgId, "MEMBER");
+ await associateUserToCompany(user2.id, company.orgId, "ADMIN", {
+ notifications: []
+ });
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -1393,7 +1443,7 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
});
const pendingBSDASRIRevisionRequest =
- await getPendingBSDASRIRevisionRequestsWithAdmins(2);
+ await getPendingBSDASRIRevisionRequestsWithSubscribers(2);
expect(pendingBSDASRIRevisionRequest.length).toEqual(1);
expect(pendingBSDASRIRevisionRequest[0].id).toEqual(request.id);
@@ -1404,12 +1454,12 @@ describe("getPendingBSDARIRevisionRequestsWithAdmins", () => {
expect(pendingBSDASRIRevisionRequest[0].approvals[0].company.id).toEqual(
company.id
);
- expect(pendingBSDASRIRevisionRequest[0].approvals[0].admins.length).toEqual(
- 1
- );
- expect(pendingBSDASRIRevisionRequest[0].approvals[0].admins[0].id).toEqual(
- user.id
- );
+ expect(
+ pendingBSDASRIRevisionRequest[0].approvals[0].subscribers.length
+ ).toEqual(1);
+ expect(
+ pendingBSDASRIRevisionRequest[0].approvals[0].subscribers[0].id
+ ).toEqual(user.id);
});
});
@@ -2002,7 +2052,7 @@ describe("addPendingApprovalsCompanyAdmins", () => {
include: { approvals: true, bsdd: { select: { readableId: true } } }
});
- const wrappedRequests = await addPendingApprovalsCompanyAdmins(
+ const wrappedRequests = await addPendingApprovalsCompanySubscribers(
requestsWithApprovals
);
diff --git a/apps/cron/src/commands/__tests__/sendmails.integration.ts b/apps/cron/src/commands/__tests__/sendmails.integration.ts
index 0efd74c17e..327c4d8a68 100644
--- a/apps/cron/src/commands/__tests__/sendmails.integration.ts
+++ b/apps/cron/src/commands/__tests__/sendmails.integration.ts
@@ -1,9 +1,11 @@
import axios from "axios";
import { resetDatabase } from "libs/back/tests-integration";
-import { CompanyType, MembershipRequestStatus } from "@prisma/client";
-import { addToMailQueue } from "back";
+import {
+ CompanyType,
+ MembershipRequestStatus,
+ UserNotification
+} from "@prisma/client";
import { prisma } from "@td/prisma";
-
import {
companyFactory,
createMembershipRequest,
@@ -16,18 +18,12 @@ import { bsdaFactory } from "back/src/bsda/__tests__/factories";
import {
sendMembershipRequestDetailsEmail,
sendPendingMembershipRequestDetailsEmail,
- sendPendingMembershipRequestToAdminDetailsEmail,
- sendPendingRevisionRequestToAdminDetailsEmail,
+ sendPendingMembershipRequestEmail,
+ sendPendingRevisionRequestEmail,
sendSecondOnboardingEmail
} from "../onboarding.helpers";
import { xDaysAgo } from "../helpers";
-// Intercept calls
-// Simulate queue error in order to test with sendMailSync
-jest.mock("back");
-(addToMailQueue as jest.Mock).mockRejectedValue(
- new Error("any queue error to bypass job queue and sendmail synchronously")
-);
-// Integration tests EMAIL_BACKEND is supposed to use axios.
+
jest.mock("axios");
const TODAY = new Date();
@@ -40,7 +36,6 @@ describe("sendSecondOnboardingEmail", () => {
afterEach(resetDatabase);
beforeEach(() => {
(axios.post as jest.Mock).mockClear();
- (addToMailQueue as jest.Mock).mockClear();
});
it.each([ONE_DAY_AGO, TWO_DAYS_AGO, FOUR_DAYS_AGO])(
@@ -80,7 +75,7 @@ describe("sendSecondOnboardingEmail", () => {
})
);
- await sendSecondOnboardingEmail(3);
+ await sendSecondOnboardingEmail(3, true);
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(0);
}
@@ -107,7 +102,7 @@ describe("sendSecondOnboardingEmail", () => {
})
);
- await sendSecondOnboardingEmail(3);
+ await sendSecondOnboardingEmail(3, true);
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(1);
expect(axios.post as jest.Mock).toHaveBeenCalledWith(
@@ -159,7 +154,7 @@ describe("sendSecondOnboardingEmail", () => {
})
);
- await sendSecondOnboardingEmail(3);
+ await sendSecondOnboardingEmail(3, true);
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(1);
expect(axios.post as jest.Mock).toHaveBeenCalledWith(
@@ -195,7 +190,6 @@ describe("sendMembershipRequestDetailsEmail", () => {
afterEach(resetDatabase);
beforeEach(() => {
(axios.post as jest.Mock).mockClear();
- (addToMailQueue as jest.Mock).mockClear();
});
it("no membership request > should not send any mail", async () => {
@@ -205,7 +199,7 @@ describe("sendMembershipRequestDetailsEmail", () => {
})
);
- await sendMembershipRequestDetailsEmail(3);
+ await sendMembershipRequestDetailsEmail(3, true);
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(0);
});
@@ -220,7 +214,7 @@ describe("sendMembershipRequestDetailsEmail", () => {
})
);
- await sendMembershipRequestDetailsEmail(3);
+ await sendMembershipRequestDetailsEmail(3, true);
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(1);
expect(axios.post as jest.Mock).toHaveBeenCalledWith(
@@ -255,7 +249,6 @@ describe("sendPendingMembershipRequestDetailsEmail", () => {
afterEach(resetDatabase);
beforeEach(() => {
(axios.post as jest.Mock).mockClear();
- (addToMailQueue as jest.Mock).mockClear();
});
it("no pending membership request > should not send any mail", async () => {
@@ -265,7 +258,7 @@ describe("sendPendingMembershipRequestDetailsEmail", () => {
})
);
- await sendPendingMembershipRequestDetailsEmail(3);
+ await sendPendingMembershipRequestDetailsEmail(3, true);
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(0);
});
@@ -286,7 +279,7 @@ describe("sendPendingMembershipRequestDetailsEmail", () => {
})
);
- await sendPendingMembershipRequestDetailsEmail(3);
+ await sendPendingMembershipRequestDetailsEmail(3, true);
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(1);
expect(axios.post as jest.Mock).toHaveBeenCalledWith(
@@ -321,7 +314,6 @@ describe("sendPendingMembershipRequestToAdminDetailsEmail", () => {
afterEach(resetDatabase);
beforeEach(() => {
(axios.post as jest.Mock).mockClear();
- (addToMailQueue as jest.Mock).mockClear();
});
it("no pending membership request > should not send any mail", async () => {
@@ -331,15 +323,20 @@ describe("sendPendingMembershipRequestToAdminDetailsEmail", () => {
})
);
- await sendPendingMembershipRequestToAdminDetailsEmail(3);
+ await sendPendingMembershipRequestEmail(3, true);
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(0);
});
- it("pending membership > should send a mail to admin", async () => {
+ it("pending membership > should send a mail to subscriber", async () => {
const user = await userFactory();
- const companyAndAdmin = await userWithCompanyFactory("ADMIN");
+ const companyAndAdmin = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.MEMBERSHIP_REQUEST] }
+ );
await createMembershipRequest(user, companyAndAdmin.company, {
createdAt: THREE_DAYS_AGO,
@@ -352,7 +349,7 @@ describe("sendPendingMembershipRequestToAdminDetailsEmail", () => {
})
);
- await sendPendingMembershipRequestToAdminDetailsEmail(3);
+ await sendPendingMembershipRequestEmail(3, true);
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(1);
expect(axios.post as jest.Mock).toHaveBeenCalledWith(
@@ -391,7 +388,6 @@ describe("sendPendingRevisionRequestToAdminDetailsEmail", () => {
afterEach(resetDatabase);
beforeEach(() => {
(axios.post as jest.Mock).mockClear();
- (addToMailQueue as jest.Mock).mockClear();
});
it("no request > should not send anything", async () => {
@@ -403,7 +399,7 @@ describe("sendPendingRevisionRequestToAdminDetailsEmail", () => {
);
// When
- await sendPendingRevisionRequestToAdminDetailsEmail(5);
+ await sendPendingRevisionRequestEmail(5, true);
// Then
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(0);
@@ -413,7 +409,12 @@ describe("sendPendingRevisionRequestToAdminDetailsEmail", () => {
// Given
// BSDD
- const { user, company } = await userWithCompanyFactory("ADMIN");
+ const { user, company } = await userWithCompanyFactory(
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
+ );
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
);
@@ -434,7 +435,10 @@ describe("sendPendingRevisionRequestToAdminDetailsEmail", () => {
});
const { user: user2, company: company2 } = await userWithCompanyFactory(
- "ADMIN"
+ "ADMIN",
+ {},
+ {},
+ { notifications: [UserNotification.REVISION_REQUEST] }
);
const { company: companyOfSomeoneElse2 } = await userWithCompanyFactory(
"ADMIN"
@@ -465,7 +469,7 @@ describe("sendPendingRevisionRequestToAdminDetailsEmail", () => {
);
// When
- await sendPendingRevisionRequestToAdminDetailsEmail(2);
+ await sendPendingRevisionRequestEmail(2, true);
// Then
expect(axios.post as jest.Mock).toHaveBeenCalledTimes(1);
diff --git a/apps/cron/src/commands/onboarding.helpers.ts b/apps/cron/src/commands/onboarding.helpers.ts
index 8159f1b0dd..9da63feda9 100644
--- a/apps/cron/src/commands/onboarding.helpers.ts
+++ b/apps/cron/src/commands/onboarding.helpers.ts
@@ -1,5 +1,5 @@
import {
- getCompaniesAndActiveAdminsByCompanyOrgIds,
+ getCompaniesAndSubscribersByCompanyOrgIds,
formatDate,
sendMail
} from "back";
@@ -16,7 +16,8 @@ import {
Company,
RevisionRequestApprovalStatus,
RevisionRequestStatus,
- User
+ User,
+ UserNotification
} from "@prisma/client";
import * as COMPANY_CONSTANTS from "@td/constants";
import {
@@ -24,10 +25,10 @@ import {
MessageVersion,
membershipRequestDetailsEmail,
pendingMembershipRequestDetailsEmail,
- pendingMembershipRequestAdminDetailsEmail,
+ pendingMembershipRequestEmail,
profesionalsSecondOnboardingEmail,
producersSecondOnboardingEmail,
- pendingRevisionRequestAdminDetailsEmail
+ pendingRevisionRequestEmail
} from "@td/mail";
import { xDaysAgo } from "./helpers";
@@ -121,7 +122,7 @@ export const getRecentlyRegisteredProducers = async (daysAgo = 2) => {
/**
* Second onboarding email. Different for profesionals & non-profesionals / producers
*/
-export const sendSecondOnboardingEmail = async (daysAgo = 2) => {
+export const sendSecondOnboardingEmail = async (daysAgo = 2, sync = false) => {
// Pros
const profesionals = await getRecentlyRegisteredProfesionals(daysAgo);
@@ -134,7 +135,7 @@ export const sendSecondOnboardingEmail = async (daysAgo = 2) => {
messageVersions: proMessageVersions
});
- await sendMail(proPayload);
+ await sendMail(proPayload, { sync });
}
// Producers. If already in pro list, remove (only 1 email, pro has priority)
@@ -155,7 +156,7 @@ export const sendSecondOnboardingEmail = async (daysAgo = 2) => {
messageVersions: producersMessageVersions
});
- await sendMail(producersPayload);
+ await sendMail(producersPayload, { sync });
}
await prisma.$disconnect();
@@ -189,7 +190,10 @@ export const getRecentlyRegisteredUsersWithNoCompanyNorMembershipRequest =
* Send a mail to users who registered recently and who haven't
* issued a single MembershipRequest yet
*/
-export const sendMembershipRequestDetailsEmail = async (daysAgo = 7) => {
+export const sendMembershipRequestDetailsEmail = async (
+ daysAgo = 7,
+ sync = false
+) => {
const recipients =
await getRecentlyRegisteredUsersWithNoCompanyNorMembershipRequest(daysAgo);
@@ -202,7 +206,7 @@ export const sendMembershipRequestDetailsEmail = async (daysAgo = 7) => {
messageVersions
});
- await sendMail(payload);
+ await sendMail(payload, { sync });
}
await prisma.$disconnect();
@@ -242,7 +246,8 @@ export const getActiveUsersWithPendingMembershipRequests = async (
* got no answer
*/
export const sendPendingMembershipRequestDetailsEmail = async (
- daysAgo = 14
+ daysAgo = 14,
+ sync = false
) => {
const recipients = await getActiveUsersWithPendingMembershipRequests(daysAgo);
@@ -255,13 +260,20 @@ export const sendPendingMembershipRequestDetailsEmail = async (
messageVersions
});
- await sendMail(payload);
+ await sendMail(payload, { sync });
}
await prisma.$disconnect();
};
-export const getPendingMembershipRequestsAndAssociatedAdmins = async (
+/**
+ * Récupère toutes les demandes de rattachement qui sont en attente depuis `daysAgo`
+ * jours ainsi que :
+ * - les établissements de rattachement correspondants.
+ * - les utilisateurs au sein de ces établissements qui sont abonnées aux notifications
+ * de demandes de rattachement par e-mail.
+ */
+export const getPendingMembershipRequestsAndAssociatedSubscribers = async (
daysAgo: number
) => {
const now = new Date();
@@ -280,7 +292,14 @@ export const getPendingMembershipRequestsAndAssociatedAdmins = async (
company: {
include: {
companyAssociations: {
- where: { role: "ADMIN", user: { isActive: true } },
+ where: {
+ notifications: {
+ has: UserNotification.MEMBERSHIP_REQUEST
+ },
+ user: {
+ isActive: true
+ }
+ },
include: { user: true }
}
}
@@ -291,47 +310,55 @@ export const getPendingMembershipRequestsAndAssociatedAdmins = async (
/**
* For each unanswered membership request issued X days ago, send an
- * email to all the admins of request's targeted company
+ * email to all the users inside the companies who are subsribed to
+ * membership requests notifications by e-mail.
*/
-export const sendPendingMembershipRequestToAdminDetailsEmail = async (
- daysAgo = 14
+export const sendPendingMembershipRequestEmail = async (
+ daysAgo = 14,
+ sync = false
) => {
- const requests = await getPendingMembershipRequestsAndAssociatedAdmins(
+ const requests = await getPendingMembershipRequestsAndAssociatedSubscribers(
daysAgo
);
if (requests.length) {
- const messageVersions: MessageVersion[] = requests.map(request => {
- const variables = {
- requestId: request.id,
- email: request.user.email,
- orgName: request.company.name,
- orgId: request.company.orgId
- };
-
- const template = renderMail(pendingMembershipRequestAdminDetailsEmail, {
- variables,
- messageVersions: []
- });
-
- return {
- to: request.company.companyAssociations.map(companyAssociation => ({
- email: companyAssociation.user.email,
- name: companyAssociation.user.name
- })),
- ...(template.body && {
- params: {
- body: template.body
- }
- })
- };
- });
+ const messageVersions: MessageVersion[] = requests
+ .map(request => {
+ const variables = {
+ requestId: request.id,
+ email: request.user.email,
+ orgName: request.company.name,
+ orgId: request.company.orgId
+ };
+
+ const template = renderMail(pendingMembershipRequestEmail, {
+ variables,
+ messageVersions: []
+ });
- const payload = renderMail(pendingMembershipRequestAdminDetailsEmail, {
+ if (request.company.companyAssociations.length) {
+ return {
+ to: request.company.companyAssociations.map(companyAssociation => ({
+ email: companyAssociation.user.email,
+ name: companyAssociation.user.name
+ })),
+ ...(template.body && {
+ params: {
+ body: template.body
+ }
+ })
+ };
+ }
+
+ return null;
+ })
+ .filter(Boolean);
+
+ const payload = renderMail(pendingMembershipRequestEmail, {
messageVersions
});
- await sendMail(payload);
+ await sendMail(payload, { sync });
}
await prisma.$disconnect();
@@ -355,19 +382,19 @@ type RequestWithApprovals =
type RequestWithWrappedApprovals =
| (BsddRevisionRequestWithReadableId & {
approvals: (BsddRevisionRequestApproval & {
- admins: User[];
+ subscribers: User[];
company: Company;
})[];
})
| (BsdaRevisionRequest & {
approvals: (BsdaRevisionRequestApproval & {
- admins: User[];
+ subscribers: User[];
company: Company;
})[];
})
| (BsdasriRevisionRequest & {
approvals: (BsdasriRevisionRequestApproval & {
- admins: User[];
+ subscribers: User[];
company: Company;
})[];
});
@@ -375,7 +402,7 @@ type RequestWithWrappedApprovals =
/**
* Will add pending approval companies' admins to requests
*/
-export const addPendingApprovalsCompanyAdmins = async (
+export const addPendingApprovalsCompanySubscribers = async (
requests: RequestWithApprovals[]
): Promise => {
// Extract all pending company sirets
@@ -389,11 +416,14 @@ export const addPendingApprovalsCompanyAdmins = async (
)
.flat();
- // Find companies and their respective admins
+ // Find companies and their respective mail subscribers
const companiesAndAdminsByOrgIds =
- await getCompaniesAndActiveAdminsByCompanyOrgIds(companySirets);
+ await getCompaniesAndSubscribersByCompanyOrgIds(
+ companySirets,
+ UserNotification.REVISION_REQUEST
+ );
- // Add admins to requests
+ // Add mail subscribers to requests
return requests.map((request: RequestWithApprovals) => {
return {
...request,
@@ -407,13 +437,14 @@ export const addPendingApprovalsCompanyAdmins = async (
.map(approval => ({
...approval,
company: companiesAndAdminsByOrgIds[approval.approverSiret],
- admins: companiesAndAdminsByOrgIds[approval.approverSiret].admins
+ subscribers:
+ companiesAndAdminsByOrgIds[approval.approverSiret].subscribers
}))
};
});
};
-export const getPendingBSDDRevisionRequestsWithAdmins = async (
+export const getPendingBSDDRevisionRequestsWithSubscribers = async (
daysAgo: number
): Promise => {
const now = new Date();
@@ -431,10 +462,10 @@ export const getPendingBSDDRevisionRequestsWithAdmins = async (
});
// Add admins to requests
- return await addPendingApprovalsCompanyAdmins(requests);
+ return await addPendingApprovalsCompanySubscribers(requests);
};
-export const getPendingBSDARevisionRequestsWithAdmins = async (
+export const getPendingBSDARevisionRequestsWithSubscribers = async (
daysAgo: number
): Promise => {
const now = new Date();
@@ -452,10 +483,10 @@ export const getPendingBSDARevisionRequestsWithAdmins = async (
});
// Add admins to requests
- return await addPendingApprovalsCompanyAdmins(requests);
+ return await addPendingApprovalsCompanySubscribers(requests);
};
-export const getPendingBSDASRIRevisionRequestsWithAdmins = async (
+export const getPendingBSDASRIRevisionRequestsWithSubscribers = async (
daysAgo: number
): Promise => {
const now = new Date();
@@ -473,19 +504,20 @@ export const getPendingBSDASRIRevisionRequestsWithAdmins = async (
});
// Add admins to requests
- return await addPendingApprovalsCompanyAdmins(requests);
+ return await addPendingApprovalsCompanySubscribers(requests);
};
/**
* Send an email to admins who didn't answer to a revision request
*/
-export const sendPendingRevisionRequestToAdminDetailsEmail = async (
- daysAgo = 5
+export const sendPendingRevisionRequestEmail = async (
+ daysAgo = 5,
+ sync = false
) => {
const pendingBsddRevisionRequest =
- await getPendingBSDDRevisionRequestsWithAdmins(daysAgo);
+ await getPendingBSDDRevisionRequestsWithSubscribers(daysAgo);
const pendingBsdaRevisionRequest =
- await getPendingBSDARevisionRequestsWithAdmins(daysAgo);
+ await getPendingBSDARevisionRequestsWithSubscribers(daysAgo);
const requests = [
...pendingBsddRevisionRequest,
@@ -510,13 +542,13 @@ export const sendPendingRevisionRequestToAdminDetailsEmail = async (
companyOrgId: approval.company.orgId
};
- const template = renderMail(pendingRevisionRequestAdminDetailsEmail, {
+ const template = renderMail(pendingRevisionRequestEmail, {
variables,
messageVersions: []
});
return {
- to: approval.admins.map(admin => ({
+ to: approval.subscribers.map(admin => ({
email: admin.email,
name: admin.name
})),
@@ -530,11 +562,11 @@ export const sendPendingRevisionRequestToAdminDetailsEmail = async (
})
.flat();
- const payload = renderMail(pendingRevisionRequestAdminDetailsEmail, {
+ const payload = renderMail(pendingRevisionRequestEmail, {
messageVersions
});
- await sendMail(payload);
+ await sendMail(payload, { sync });
}
await prisma.$disconnect();
diff --git a/apps/cron/src/cron.test.ts b/apps/cron/src/cron.test.ts
index 4bd2fad6c4..893f577b08 100644
--- a/apps/cron/src/cron.test.ts
+++ b/apps/cron/src/cron.test.ts
@@ -1,4 +1,4 @@
-import { validateOnbardingCronSchedule } from "./main";
+import { validateDailyCronSchedule } from "./main";
jest.mock("back", () => ({
initSentry: jest.fn()
@@ -8,9 +8,9 @@ jest.mock("./commands/appendix1.helpers", () => ({
cleanUnusedAppendix1ProducerBsdds: jest.fn(() => Promise.resolve())
}));
-describe("validateOnbardingCronSchedule", () => {
+describe("validateDailyCronSchedule", () => {
it("should throw error when invalid quartz expression", () => {
- expect(() => validateOnbardingCronSchedule("80 8 * * *")).toThrow(
+ expect(() => validateDailyCronSchedule("80 8 * * *")).toThrow(
"Invalid CRON expression : 80 8 * * *"
);
});
@@ -18,15 +18,13 @@ describe("validateOnbardingCronSchedule", () => {
it("should throw an error if valid CRON expression but not set to run once every day", () => {
// At 00:00 on day-of-month 1.
const everyFistOfMonthAtMidnight = "* * 1 * *";
- expect(() =>
- validateOnbardingCronSchedule(everyFistOfMonthAtMidnight)
- ).toThrow(
+ expect(() => validateDailyCronSchedule(everyFistOfMonthAtMidnight)).toThrow(
"CRON expression should be set to run once every day : {m} {h} * * *"
);
});
it("should return true for valid expression", () => {
- expect(validateOnbardingCronSchedule("8 8 * * *")).toEqual(true);
- expect(validateOnbardingCronSchedule("08 08 * * *")).toEqual(true);
+ expect(validateDailyCronSchedule("8 8 * * *")).toEqual(true);
+ expect(validateDailyCronSchedule("08 08 * * *")).toEqual(true);
});
});
diff --git a/apps/cron/src/main.ts b/apps/cron/src/main.ts
index 07672e0272..d13bf2723c 100644
--- a/apps/cron/src/main.ts
+++ b/apps/cron/src/main.ts
@@ -1,16 +1,17 @@
import * as cron from "cron";
import cronValidator from "cron-validate";
-import { initSentry } from "back";
+import { cleanUpIsReturnForTab, initSentry } from "back";
import {
sendMembershipRequestDetailsEmail,
sendPendingMembershipRequestDetailsEmail,
- sendPendingMembershipRequestToAdminDetailsEmail,
- sendPendingRevisionRequestToAdminDetailsEmail,
+ sendPendingMembershipRequestEmail,
+ sendPendingRevisionRequestEmail,
sendSecondOnboardingEmail
} from "./commands/onboarding.helpers";
import { cleanAppendix1 } from "./commands/appendix1.helpers";
-const { CRON_ONBOARDING_SCHEDULE, TZ } = process.env;
+const { CRON_ONBOARDING_SCHEDULE, CRON_CLEANUP_IS_RETURN_TAB_SCHEDULE, TZ } =
+ process.env;
let jobs: cron.CronJob[] = [
new cron.CronJob({
@@ -23,7 +24,7 @@ let jobs: cron.CronJob[] = [
];
if (CRON_ONBOARDING_SCHEDULE) {
- validateOnbardingCronSchedule(CRON_ONBOARDING_SCHEDULE);
+ validateDailyCronSchedule(CRON_ONBOARDING_SCHEDULE);
jobs = [
...jobs,
@@ -55,7 +56,7 @@ if (CRON_ONBOARDING_SCHEDULE) {
new cron.CronJob({
cronTime: CRON_ONBOARDING_SCHEDULE,
onTick: async () => {
- await sendPendingMembershipRequestToAdminDetailsEmail();
+ await sendPendingMembershipRequestEmail();
},
timeZone: TZ
}),
@@ -63,7 +64,23 @@ if (CRON_ONBOARDING_SCHEDULE) {
new cron.CronJob({
cronTime: CRON_ONBOARDING_SCHEDULE,
onTick: async () => {
- await sendPendingRevisionRequestToAdminDetailsEmail();
+ await sendPendingRevisionRequestEmail();
+ },
+ timeZone: TZ
+ })
+ ];
+}
+
+if (CRON_CLEANUP_IS_RETURN_TAB_SCHEDULE) {
+ validateDailyCronSchedule(CRON_CLEANUP_IS_RETURN_TAB_SCHEDULE);
+
+ jobs = [
+ ...jobs,
+ // cleanup the isReturnFor tab
+ new cron.CronJob({
+ cronTime: CRON_CLEANUP_IS_RETURN_TAB_SCHEDULE,
+ onTick: async () => {
+ await cleanUpIsReturnForTab();
},
timeZone: TZ
})
@@ -88,7 +105,7 @@ if (Sentry) {
jobs.forEach(job => job.start());
-export function validateOnbardingCronSchedule(cronExp: string) {
+export function validateDailyCronSchedule(cronExp: string) {
// checks it is a valid cron quartz expression
const isValid = cronValidator(cronExp).isValid();
diff --git a/apps/doc/docs/guides/playground.md b/apps/doc/docs/guides/playground.md
index 96ff93ee9a..da1eb6a3aa 100644
--- a/apps/doc/docs/guides/playground.md
+++ b/apps/doc/docs/guides/playground.md
@@ -23,7 +23,7 @@ Le playground GraphQL est composé de différentes zones :
## Renseigner son token
-Le token (voir [Authentification](../reference/authentification)) doit être renseigné dans l'onglet "HTTP Headers" de la façon suivante :
+Le token (voir [Authentification](../reference/authentification.md)) doit être renseigné dans l'onglet "HTTP Headers" de la façon suivante :

## Exécuter une requête GraphQL
@@ -44,7 +44,7 @@ Vous pouvez également utiliser l'onglet "Variables" pour injecter les variables
## Parcourir la documentation de l'API
-L'onglet de droite "Docs" vous permet de parcourir la référence de l'API. Vous y retrouverez les différentes Query et Mutation disponibles ainsi que les variables et les types de retours. La référence de l'API est également disponible dans la section [Référence API](../reference/api-reference/bsdd/queries)
+L'onglet de droite "Docs" vous permet de parcourir la référence de l'API. Vous y retrouverez les différentes Query et Mutation disponibles ainsi que les variables et les types de retours. La référence de l'API est également disponible dans la section [Référence API](../reference/api-reference/bsdd/queries.md)

diff --git a/apps/doc/docs/guides/registre.md b/apps/doc/docs/guides/registre.md
index 220e553ef6..570fd9b4c5 100644
--- a/apps/doc/docs/guides/registre.md
+++ b/apps/doc/docs/guides/registre.md
@@ -7,13 +7,13 @@ L'[arrêté du 31 mai 2021](https://www.legifrance.gouv.fr/jorf/id/JORFTEXT00004
## Export JSON
Les `queries` permettant d'exporter les données registre sont les suivantes :
-- [`incomingWastes`](../reference/api-reference/registre/queries#incomingwastes) : registre de déchets entrants
-- [`outgoingWastes`](../reference/api-reference/registre/queries#outgoingwastes) : registre de déchets sortants
-- [`transporteWastes`](../reference/api-reference/registre/queries#transportedwastes): registre de déchets transportés
-- [`managedWastes`](../reference/api-reference/registre/queries#managedwastes) : registre de déchets gérés (courtage ou négoce)
-- [`allWastes`](../reference/api-reference/registre/queries#allwastes) : registre permettant d'exporter toutes les données de bordereaux pour un ou plusieurs établissements
+- [`incomingWastes`](../reference/api-reference/registre/queries.md#incomingwastes) : registre de déchets entrants
+- [`outgoingWastes`](../reference/api-reference/registre/queries.md#outgoingwastes) : registre de déchets sortants
+- [`transporteWastes`](../reference/api-reference/registre/queries.md#transportedwastes): registre de déchets transportés
+- [`managedWastes`](../reference/api-reference/registre/queries.md#managedwastes) : registre de déchets gérés (courtage ou négoce)
+- [`allWastes`](../reference/api-reference/registre/queries.md#allwastes) : registre permettant d'exporter toutes les données de bordereaux pour un ou plusieurs établissements
-Des filtres avancés peuvent être appliqués pour restreindre les données exportés par code déchet, quantité, date d'expédition, etc, et son décrits dans l'objet [RegisterWhere](../reference/api-reference/registre/inputObjects#wasteregistrywhere).
+Des filtres avancés peuvent être appliqués pour restreindre les données exportés par code déchet, quantité, date d'expédition, etc, et son décrits dans l'objet [RegisterWhere](../reference/api-reference/registre/inputObjects.md#wasteregistrywhere).
Exemple de requête :
@@ -102,7 +102,7 @@ Les résultats sont paginés. Pour récupérer tous les déchets :
Les données peuvent également être téléchargées au format `CSV` ou Excel (`XLXS`).
-Pour ce faire vous devez utiliser la query [`wastesRegistryCsv`](../reference/api-reference/registre/queries#wastesregistrycsv) ou [`wastesRegistryXls`](../reference/api-reference/registre/queries#wastesregistryxls) de la façon suivante :
+Pour ce faire vous devez utiliser la query [`wastesRegistryCsv`](../reference/api-reference/registre/queries.md#wastesregistrycsv) ou [`wastesRegistryXls`](../reference/api-reference/registre/queries.md#wastesregistryxls) de la façon suivante :
```graphql
query {
diff --git a/apps/doc/docs/guides/sirene.md b/apps/doc/docs/guides/sirene.md
index 54c75262aa..3bd3db79f9 100644
--- a/apps/doc/docs/guides/sirene.md
+++ b/apps/doc/docs/guides/sirene.md
@@ -4,11 +4,11 @@ title: Rechercher un établissement partenaire sur l'API Trackdéchets
# Par son nom, ou par n° SIRET ou par son n°TVA intracommunautaire pour les entreprises européennes.
-Nous exposons une query [`searchCompanies`](../reference/api-reference/user-company/queries#searchcompanies) qui interroge la base SIRENE (via [les données ouvertes de l'INSEE](https://files.data.gouv.fr/insee-sirene/)), ou la base VIES (via [le service la commission européenne](https://ec.europa.eu/taxation_customs/vies/)) la base des installations classées pour la protection de l'environnement (ICPE) et la base Trackdéchets pour obtenir des informations sur un établissement à partir de son numéro SIRET, sa raison sociale, ou son numéro de TVA intra-communautaire.
+Nous exposons une query [`searchCompanies`](../reference/api-reference/user-company/queries.md#searchcompanies) qui interroge la base SIRENE (via [les données ouvertes de l'INSEE](https://files.data.gouv.fr/insee-sirene/)), ou la base VIES (via [le service la commission européenne](https://ec.europa.eu/taxation_customs/vies/)) la base des installations classées pour la protection de l'environnement (ICPE) et la base Trackdéchets pour obtenir des informations sur un établissement à partir de son numéro SIRET, sa raison sociale, ou son numéro de TVA intra-communautaire.
Elle requiert un token d'API Trackdéchets et permet d'accéder à toutes les informations d'un établissement sur Trackdéchet et à jour des bases de l'INSEE ou de VIES (via [le service la commission européenne](https://ec.europa.eu/taxation_customs/vies/)).
-La query renvoie un objet de type [`CompanySearchResult`](../reference/api-reference/user-company/objects#companysearchresult) et permet notamment de savoir si un établissement est inscrit sur Trackdéchets grâce au champ `isRegistered`, mais aussi les coordonnées de l'établissement, comme le type d'établissement sur Trackdéchets.
+La query renvoie un objet de type [`CompanySearchResult`](../reference/api-reference/user-company/objects.md#companysearchresult) et permet notamment de savoir si un établissement est inscrit sur Trackdéchets grâce au champ `isRegistered`, mais aussi les coordonnées de l'établissement, comme le type d'établissement sur Trackdéchets.
Pour retourner les établissements étrangers quand on cherche par `clue : "NUMERODETVA"`, il faut activer à `true` le paramètre `allowForeignCompanies`.
@@ -47,9 +47,9 @@ query {
# Par son n° SIRET pour les entreprises françaises ou par son n°TVA intracommunautaire pour les entreprises européennes.
-Nous exposons une query [`companyInfos`](../reference/api-reference/user-company/queries#companyinfos) qui interroge la base SIRENE (via [les données ouvertes de l'INSEE](https://files.data.gouv.fr/insee-sirene/)), ou la base VIES (via [le service la commission européenne](https://ec.europa.eu/taxation_customs/vies/)) la base des installations classées pour la protection de l'environnement (ICPE) et la base Trackdéchets pour obtenir des informations sur un établissement à partir de son numéro SIRET.
+Nous exposons une query [`companyInfos`](../reference/api-reference/user-company/queries.md#companyinfos) qui interroge la base SIRENE (via [les données ouvertes de l'INSEE](https://files.data.gouv.fr/insee-sirene/)), ou la base VIES (via [le service la commission européenne](https://ec.europa.eu/taxation_customs/vies/)) la base des installations classées pour la protection de l'environnement (ICPE) et la base Trackdéchets pour obtenir des informations sur un établissement à partir de son numéro SIRET.
-La requête renvoie un objet de type [`CompanyPublic`](../reference/api-reference/user-company/objects#companypublic) et permet notamment de savoir si un établissement est inscrit sur Trackdéchets grâce au champ `isRegistered`. Si l'établissement demandé est enregistré auprès de l'INSEE comme "non-diffusible" ou "protégé" (c'est-à-dire si `statutDiffusionEtablissement` ne renvoie pas "O"), nous ne les révèlerons pas dans cette requête `companyInfos`. Il faudra utiliser la requête authentifiée `searchCompanies` documentée sur cette page en passant le siret comme valeur de la variable `clue`.
+La requête renvoie un objet de type [`CompanyPublic`](../reference/api-reference/user-company/objects.md#companypublic) et permet notamment de savoir si un établissement est inscrit sur Trackdéchets grâce au champ `isRegistered`. Si l'établissement demandé est enregistré auprès de l'INSEE comme "non-diffusible" ou "protégé" (c'est-à-dire si `statutDiffusionEtablissement` ne renvoie pas "O"), nous ne les révèlerons pas dans cette requête `companyInfos`. Il faudra utiliser la requête authentifiée `searchCompanies` documentée sur cette page en passant le siret comme valeur de la variable `clue`.
Exemple d'utilisation:
diff --git a/apps/doc/docs/guides/webhooks.md b/apps/doc/docs/guides/webhooks.md
index 2f26e4b508..1d3c6ad205 100644
--- a/apps/doc/docs/guides/webhooks.md
+++ b/apps/doc/docs/guides/webhooks.md
@@ -11,7 +11,7 @@ L'utilisation des Webhooks permet aux Systèmes d'Information de réduire la né
Pour recevoir des Webhooks, vous devez suivre ces étapes :
-1. Configurer les Webhooks en utilisant l'API des ["WebhookSettings"](../reference/api-reference/webhooks/mutations#createwebhooksetting).
+1. Configurer les Webhooks en utilisant l'API des ["WebhookSettings"](../reference/api-reference/webhooks/mutations.md#createwebhooksetting).
2. Autoriser votre SI à recevoir des requêtes HTTP de Trackdéchets.
3. Effectuer des modifications sur les bordereaux contenant les établissements correspondants aux "WebhookSettings" ou attendre que d'autres acteurs le fassent.
4. Recevez la requête de notification de mise à jour sur l'URL de votre SI configuré comme Webhook.
@@ -21,7 +21,7 @@ Pour recevoir des Webhooks, vous devez suivre ces étapes :
L'API des "WebhookSettings" vous permet de configurer l'envoi des Webhooks avec les principes suivants :
- L'API est accessible uniquement aux utilisateurs ADMIN.
-- Un seul Webhook est autorisé par établissement (identifié par [`companyId`](../reference/api-reference/webhooks/inputObjects#webhooksettingcreateinput)).
+- Un seul Webhook est autorisé par établissement (identifié par [`companyId`](../reference/api-reference/webhooks/inputObjects.md#webhooksettingcreateinput)).
- Une URL de notification sur votre SI est associée à chaque Webhook.
- Un token d'au moins 20 caractères est requis lors de la création du "WebhookSetting" et sera transmis en tant qu'en-tête de la requête du Webhook (Authorization: Bearer: token).
- Le token n'est pas visible dans les requêtes, mais vous pouvez le mettre à jour à tout moment.
diff --git a/apps/doc/docs/reference/authentification.md b/apps/doc/docs/reference/authentification.md
index 7b4812b82c..a0c2938c4b 100644
--- a/apps/doc/docs/reference/authentification.md
+++ b/apps/doc/docs/reference/authentification.md
@@ -21,4 +21,4 @@ Un token est lié à un utilisateur. Les permissions du token découle donc dire
## Authentification pour le compte de tiers
-Pour les logiciels (ex : logiciel SaaS déchets) désirant se connecter à l'API Trackdéchets pour le compte d'utilisateurs tiers, nous recommandons d'utiliser le protocole OAuth2 : [Créer une application OAuth2](../guides/oauth2)
\ No newline at end of file
+Pour les logiciels (ex : logiciel SaaS déchets) désirant se connecter à l'API Trackdéchets pour le compte d'utilisateurs tiers, nous recommandons d'utiliser le protocole OAuth2 : [Créer une application OAuth2](../guides/oauth2.md)
\ No newline at end of file
diff --git a/apps/doc/docs/reference/statuts/bsda.mdx b/apps/doc/docs/reference/statuts/bsda.mdx
index d30694c710..8d2aefafad 100644
--- a/apps/doc/docs/reference/statuts/bsda.mdx
+++ b/apps/doc/docs/reference/statuts/bsda.mdx
@@ -4,7 +4,7 @@ title: BSDA
import Mermaid from '../../../src/components/Mermaid';
-Au cours de son cycle de vie, le BSDA peut passer par différents états décrits [ici](../api-reference/bsdasri/enums#bsdasristatus).
+Au cours de son cycle de vie, le BSDA peut passer par différents états décrits [ici](../api-reference/bsdasri/enums.md#bsdasristatus).
- `INITIAL` (initial) : C'est l'état dans lequel le Bsda est créé. `readableId` est affecté.
diff --git a/apps/doc/docs/reference/statuts/bsdasri.mdx b/apps/doc/docs/reference/statuts/bsdasri.mdx
index 2d6e6e315d..0e5c267f77 100644
--- a/apps/doc/docs/reference/statuts/bsdasri.mdx
+++ b/apps/doc/docs/reference/statuts/bsdasri.mdx
@@ -4,7 +4,7 @@ title: BSDASRI
import Mermaid from '../../../src/components/Mermaid';
-Au cours de son cycle de vie, le BSDASRI peut passer par différents états décrits [ici](../api-reference/bsdasri/enums#bsdasristatus).
+Au cours de son cycle de vie, le BSDASRI peut passer par différents états décrits [ici](../api-reference/bsdasri/enums.md#bsdasristatus).
- `INITIAL` (initial) : C'est l'état dans lequel le dasri est créé. `readableId` est affecté.
diff --git a/apps/doc/docs/reference/statuts/bsdd.mdx b/apps/doc/docs/reference/statuts/bsdd.mdx
index 7e773d0ae2..299e9a7483 100644
--- a/apps/doc/docs/reference/statuts/bsdd.mdx
+++ b/apps/doc/docs/reference/statuts/bsdd.mdx
@@ -29,19 +29,19 @@ Chaque changement d'état s'effectue grâce à une mutation.
| Mutation | Transition | Données | Permissions |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `createForm` | `-> DRAFT` | [FormInput](../api-reference/bsdd/inputObjects#forminput) | émetteur destinataire transporteur négociant éco-organisme |
-| `updateForm` | `DRAFT -> DRAFT` `SEALED -> SEALED` | [FormInput](../api-reference/bsdd/inputObjects#forminput) | émetteur destinataire transporteur négociant éco-organisme |
+| `createForm` | `-> DRAFT` | [FormInput](../api-reference/bsdd/inputObjects.md#forminput) | émetteur destinataire transporteur négociant éco-organisme |
+| `updateForm` | `DRAFT -> DRAFT` `SEALED -> SEALED` | [FormInput](../api-reference/bsdd/inputObjects.md#forminput) | émetteur destinataire transporteur négociant éco-organisme |
| `markAsSealed` | `DRAFT -> SEALED` | | émetteur destinataire transporteur négociant éco-organisme |
-| `signEmissionForm` | `SEALED -> SIGNED_BY_PRODUCER` `RESEALED -> SIGNED_BY_TEMP_STORER` | [SignEmissionFormInput](../api-reference/bsdd/inputObjects#signemissionforminput) | émetteur / entreposage provisoire (authentifié ou via son code de signature) éco-organisme (authentifié ou via son code de signature) |
-| `signTransportForm` | `SIGNED_BY_PRODUCER -> SENT` `SIGNED_BY_TEMP_STORER -> RESENT` | [SignTransportFormInput](../api-reference/bsdd/inputObjects#signtransportforminput) | transporteur (authentifié ou via son code de signature) |
-| `markAsReceived` | `SENT -> ACCEPTED` `SENT -> RECEIVED` `SENT -> REFUSED` | [ReceivedFormInput](../api-reference/bsdd/inputObjects#receivedforminput) | Uniquement le destinataire du BSD |
-| `markAsAccepted` | `RECEIVED -> ACCEPTED` | [AcceptedFormInput](../api-reference/bsdd/inputObjects#acceptedforminput) | Uniquement le destinataire du BSD |
-| `markAsProcessed` | `RECEIVED -> PROCESSED` `RECEIVED -> NO_TRACEABILITY` `RECEIVED -> AWAITING_GROUP` `RECEIVED -> FOLLOWED_WITH_PNTTD` | [ProcessedFormInput](../api-reference/bsdd/inputObjects#processedforminput) | Uniquement le destinataire du BSD |
-| `markAsTempStored` | `SENT -> TEMP_STORER_ACCEPTED` `SENT -> TEMP_STORED` `SENT -> REFUSED` | [TempStoredFormInput](../api-reference/bsdd/inputObjects#tempstoredforminput) | Uniquement le site d'entreposage temporaire ou de reconditionnement |
-| `markAsTempStorerAccepted` | `TEMP_STORED -> TEMP_STORER_ACCEPTED` | [TempStorerAcceptedFormInput](../api-reference/bsdd/inputObjects#tempstoreracceptedforminput) | Uniquement le site d'entreposage temporaire ou de reconditionnement |
-| `markAsResealed` | `TEMP_STORED -> RESEALED` `RESEALED -> RESEALED` | [ResealedFormInput](../api-reference/bsdd/inputObjects#resealedforminput) | Uniquement le site d'entreposage temporaire ou de reconditionnement |
-| `importPaperForm` | `SEALED -> PROCESSED` | [ImportPaperFormInput](../api-reference/bsdd/inputObjects#importpaperforminput) | Uniquement l'entreprise de destination |
-| `createFormRevisionRequest` | `CANCELED` | [CreateFormRevisionRequestInput](../api-reference/bsdd/inputObjects#createformrevisionrequestinput) | |
+| `signEmissionForm` | `SEALED -> SIGNED_BY_PRODUCER` `RESEALED -> SIGNED_BY_TEMP_STORER` | [SignEmissionFormInput](../api-reference/bsdd/inputObjects.md#signemissionforminput) | émetteur / entreposage provisoire (authentifié ou via son code de signature) éco-organisme (authentifié ou via son code de signature) |
+| `signTransportForm` | `SIGNED_BY_PRODUCER -> SENT` `SIGNED_BY_TEMP_STORER -> RESENT` | [SignTransportFormInput](../api-reference/bsdd/inputObjects.md#signtransportforminput) | transporteur (authentifié ou via son code de signature) |
+| `markAsReceived` | `SENT -> ACCEPTED` `SENT -> RECEIVED` `SENT -> REFUSED` | [ReceivedFormInput](../api-reference/bsdd/inputObjects.md#receivedforminput) | Uniquement le destinataire du BSD |
+| `markAsAccepted` | `RECEIVED -> ACCEPTED` | [AcceptedFormInput](../api-reference/bsdd/inputObjects.md#acceptedforminput) | Uniquement le destinataire du BSD |
+| `markAsProcessed` | `RECEIVED -> PROCESSED` `RECEIVED -> NO_TRACEABILITY` `RECEIVED -> AWAITING_GROUP` `RECEIVED -> FOLLOWED_WITH_PNTTD` | [ProcessedFormInput](../api-reference/bsdd/inputObjects.md#processedforminput) | Uniquement le destinataire du BSD |
+| `markAsTempStored` | `SENT -> TEMP_STORER_ACCEPTED` `SENT -> TEMP_STORED` `SENT -> REFUSED` | [TempStoredFormInput](../api-reference/bsdd/inputObjects.md#tempstoredforminput) | Uniquement le site d'entreposage temporaire ou de reconditionnement |
+| `markAsTempStorerAccepted` | `TEMP_STORED -> TEMP_STORER_ACCEPTED` | [TempStorerAcceptedFormInput](../api-reference/bsdd/inputObjects.md#tempstoreracceptedforminput) | Uniquement le site d'entreposage temporaire ou de reconditionnement |
+| `markAsResealed` | `TEMP_STORED -> RESEALED` `RESEALED -> RESEALED` | [ResealedFormInput](../api-reference/bsdd/inputObjects.md#resealedforminput) | Uniquement le site d'entreposage temporaire ou de reconditionnement |
+| `importPaperForm` | `SEALED -> PROCESSED` | [ImportPaperFormInput](../api-reference/bsdd/inputObjects.md#importpaperforminput) | Uniquement l'entreprise de destination |
+| `createFormRevisionRequest` | `CANCELED` | [CreateFormRevisionRequestInput](../api-reference/bsdd/inputObjects.md#createformrevisionrequestinput) | |
Le diagramme ci dessous retrace le cycle de vie d'un BSD dans Trackdéchets:
diff --git a/apps/doc/docs/reference/statuts/bsff.mdx b/apps/doc/docs/reference/statuts/bsff.mdx
index 4426346067..33315265a2 100644
--- a/apps/doc/docs/reference/statuts/bsff.mdx
+++ b/apps/doc/docs/reference/statuts/bsff.mdx
@@ -4,7 +4,7 @@ title: BSFF
import Mermaid from "../../../src/components/Mermaid";
-Au cours de son cycle de vie, le BSFF passe par différents statuts décrits [ici](../api-reference/bsff/enums#bsffstatus).
+Au cours de son cycle de vie, le BSFF passe par différents statuts décrits [ici](../api-reference/bsff/enums.md#bsffstatus).
Le diagramme ci dessous retrace le cycle de vie d'un BSFF dans Trackdéchets :
diff --git a/apps/doc/docs/reference/statuts/bspaoh.mdx b/apps/doc/docs/reference/statuts/bspaoh.mdx
index 4f77152036..45cd1e9abe 100644
--- a/apps/doc/docs/reference/statuts/bspaoh.mdx
+++ b/apps/doc/docs/reference/statuts/bspaoh.mdx
@@ -4,7 +4,7 @@ title: BSPAOH
import Mermaid from '../../../src/components/Mermaid';
-Au cours de son cycle de vie, le BSPAOH peut passer par différents états décrits [ici](../api-reference/bspaoh/enums/#bspaohstatus).
+Au cours de son cycle de vie, le BSPAOH peut passer par différents états décrits [ici](../api-reference/bspaoh/enums.md#bspaohstatus).
- `INITIAL` (initial) : C'est l'état dans lequel le PAOH est créé.
diff --git a/apps/doc/docs/tutoriels/courant/query-bordereaux.md b/apps/doc/docs/tutoriels/courant/query-bordereaux.md
index 2677b69a91..e0b435d92e 100644
--- a/apps/doc/docs/tutoriels/courant/query-bordereaux.md
+++ b/apps/doc/docs/tutoriels/courant/query-bordereaux.md
@@ -6,7 +6,7 @@ Les bordereaux Bsda, Bsdasri, Bsff et Bsvhu, ont bénéficié des retours utiili
Veuillez noter que les Bsdd (requête forms) ne disposent pas des mêmes filtres.
-Pour une documentation exhaustive, veuillez consulter la référence des requêtes de chaque bordereau, par exemple [la requête bsdasri](../../reference/api-reference/bsdasri/queries#bsdasris).
+Pour une documentation exhaustive, veuillez consulter la référence des requêtes de chaque bordereau, par exemple [la requête bsdasri](../../reference/api-reference/bsdasri/queries.md#bsdasris).
Les exemples suivants portent sur les dasris, mais sont aisément transposables aux autres bordereaux.
Ils ne prétendent pas avoir un intérêt métier particulier, mais simplement expliciter la syntaxe de requête.
@@ -65,7 +65,7 @@ query {
```
#### Filtres temporels
-Les opérateurs et formats de date acceptés sont documentés dans [la référence de DateFilter](../../reference/api-reference/bsdasri/inputObjects#datefilter).
+Les opérateurs et formats de date acceptés sont documentés dans [la référence de DateFilter](../../reference/api-reference/bsdasri/inputObjects.md#datefilter).
Renvoie les dasris dont la date de création est égale ou postérieure au 23/11/2021.
diff --git a/apps/doc/docs/tutoriels/quickstart/first-query.md b/apps/doc/docs/tutoriels/quickstart/first-query.md
index ff5339db7a..238d470f61 100644
--- a/apps/doc/docs/tutoriels/quickstart/first-query.md
+++ b/apps/doc/docs/tutoriels/quickstart/first-query.md
@@ -60,7 +60,7 @@ puis exécuter la requête à l'aide du bouton "Play" au milieu. Vous devrez rec
Bravo, vous venez d'effectuer votre première requête à l'API Trackdéchets 🎉. En terminologie GraphQL, la requête ci-dessous est une `query`. Ce genre de requête se comporte comme un `GET` dans le standard REST, c'est à dire qu'elle permet de lire des données mais pas d'en modifier. Il existe aussi un autre type de requête appelée `mutation` qui va nous permettre de créer et modifier des ressources à l'instar d'un `POST` / `PUT` / `PATCH` en standard `REST`. C'est ce que nous allons voir à l'étape suivante pour la création de votre premier bordereau.
:::tip
-Les arguments et le type de retour de chaque `query` ou `mutation` sont documentés dans la référence de l'API. Exemple avec [la requête que nous venons d'effectuer](../../reference/api-reference/user-company/queries#me).
+Les arguments et le type de retour de chaque `query` ou `mutation` sont documentés dans la référence de l'API. Exemple avec [la requête que nous venons d'effectuer](../../reference/api-reference/user-company/queries.md#me).
:::
diff --git a/apps/doc/docs/tutoriels/quickstart/introduction.md b/apps/doc/docs/tutoriels/quickstart/introduction.md
index 1830ee035d..f5801d75cc 100644
--- a/apps/doc/docs/tutoriels/quickstart/introduction.md
+++ b/apps/doc/docs/tutoriels/quickstart/introduction.md
@@ -14,7 +14,7 @@ Vous y apprendrez comment réaliser des requêtes GraphQL depuis le playground w
4. [Créer votre premier bordereau](tutoriels/quickstart/first-bsd)
:::info
-Une connaissance approfondie du standard GraphQL utilisée par l'API n'est pas requise mais vous pouvez au besoin vous référer à [Introduction à GraphQL](../../concepts/graphql).
+Une connaissance approfondie du standard GraphQL utilisée par l'API n'est pas requise mais vous pouvez au besoin vous référer à [Introduction à GraphQL](../../concepts/graphql.md).
:::
diff --git a/apps/doc/docusaurus.config.js b/apps/doc/docusaurus.config.js
index caac9d64c6..5fd08c1a9d 100644
--- a/apps/doc/docusaurus.config.js
+++ b/apps/doc/docusaurus.config.js
@@ -19,6 +19,13 @@ module.exports = {
colorMode: {
disableSwitch: true
},
+ algolia: {
+ // L'ID de l'application fourni par Algolia
+ appId: '2UK4UF9U8K',
+ // Clé d'API publique : il est possible de la committer en toute sécurité
+ apiKey: 'cd706026bcf0dd0df345c4a4f450f844',
+ indexName: 'trackdechets',
+ },
navbar: {
title: "Trackdéchets",
logo: {
diff --git a/back/prisma/scripts/createGovernementAccounts.ts b/back/prisma/scripts/createGovernementAccounts.ts
deleted file mode 100644
index 1a7a7c567d..0000000000
--- a/back/prisma/scripts/createGovernementAccounts.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { prisma } from "@td/prisma";
-import { registerUpdater, Updater } from "./helper/helper";
-
-@registerUpdater(
- "Create government accounts",
- "Create government accounts",
- false
-)
-export class CreateGovernementAccounts implements Updater {
- async run() {
- const registreNationalUsers = await prisma.user.findMany({
- where: { isRegistreNational: true }
- });
- for (const user of registreNationalUsers) {
- if (!user.governmentAccountId) {
- const accountName = user.email.includes("gerep") ? "GEREP" : "RNDTS";
- await prisma.user.update({
- where: { id: user.id },
- data: {
- governmentAccount: {
- create: {
- permissions: ["REGISTRY_CAN_READ_ALL"],
- name: accountName,
- authorizedOrgIds: ["ALL"],
- authorizedIPs: [] // à compléter à la mano
- }
- }
- }
- });
- }
- }
- }
-}
diff --git a/back/src/__tests__/factories.ts b/back/src/__tests__/factories.ts
index 742e666601..348b153e08 100644
--- a/back/src/__tests__/factories.ts
+++ b/back/src/__tests__/factories.ts
@@ -11,11 +11,14 @@ import {
User,
Prisma,
Company,
- TransportMode
+ TransportMode,
+ UserNotification
} from "@prisma/client";
import { prisma } from "@td/prisma";
import { hashToken } from "../utils";
import { createUser, getUserCompanies } from "../users/database";
+import { getFormSiretsByRole, SIRETS_BY_ROLE_INCLUDE } from "../forms/database";
+import { CompanyRole } from "../common/validation/zod/schema";
/**
* Create a user with name and email
@@ -135,11 +138,23 @@ export const userWithCompanyFactory = async (
): Promise => {
const company = await companyFactory(companyOpts);
+ const notifications =
+ role === "ADMIN"
+ ? [
+ UserNotification.MEMBERSHIP_REQUEST,
+ UserNotification.REVISION_REQUEST,
+ UserNotification.BSD_REFUSAL,
+ UserNotification.SIGNATURE_CODE_RENEWAL,
+ UserNotification.BSDA_FINAL_DESTINATION_UPDATE
+ ]
+ : [];
+
const user = await userFactory({
...userOpts,
companyAssociations: {
create: {
company: { connect: { id: company.id } },
+ notifications,
role: role,
...companyAssociationOpts
}
@@ -148,6 +163,27 @@ export const userWithCompanyFactory = async (
return { user, company };
};
+/**
+ * Create a user and add him to an existing company
+ */
+export const userInCompany = async (
+ role: UserRole = "ADMIN",
+ companyId: string,
+ userOpts: Partial = {}
+) => {
+ const user = await userFactory({
+ ...userOpts,
+ companyAssociations: {
+ create: {
+ company: { connect: { id: companyId } },
+ role: role
+ }
+ }
+ });
+
+ return user;
+};
+
export const destinationFactory = async (
companyOpts: Partial = {}
) => {
@@ -350,7 +386,7 @@ export const bsddTransporterFactory = async ({
opts: Omit;
}) => {
const count = await prisma.bsddTransporter.count({ where: { formId } });
- return prisma.bsddTransporter.create({
+ const transporter = await prisma.bsddTransporter.create({
data: {
form: { connect: { id: formId } },
...bsddTransporterData,
@@ -358,6 +394,24 @@ export const bsddTransporterFactory = async ({
...opts
}
});
+
+ const form = await prisma.form.findFirstOrThrow({
+ where: { id: formId },
+ include: {
+ ...SIRETS_BY_ROLE_INCLUDE,
+ forwardedIn: true,
+ transporters: true
+ }
+ });
+
+ // Fix fields like "recipientsSirets" or "transportersSirets"
+ const denormalizedSirets = getFormSiretsByRole(form as any); // Ts doesn't infer correctly because of the boolean
+ await prisma.form.update({
+ where: { id: formId },
+ data: { ...denormalizedSirets, updatedAt: form.updatedAt }
+ });
+
+ return transporter;
};
export const upsertBaseSiret = async siret => {
@@ -423,14 +477,31 @@ export const formFactory = async ({
}
};
- return prisma.form.create({
+ const form = await prisma.form.create({
data: {
readableId: getReadableId(),
...formParams,
owner: { connect: { id: ownerId } }
},
+ include: {
+ ...SIRETS_BY_ROLE_INCLUDE,
+ forwardedIn: true,
+ transporters: true
+ }
+ });
+
+ // Fix fields like "recipientsSirets" or "transportersSirets"
+ const denormalizedSirets = getFormSiretsByRole(form as any); // Ts doesn't infer correctly because of the boolean
+ const updated = await prisma.form.update({
+ where: { id: form.id },
+ data: {
+ ...denormalizedSirets,
+ updatedAt: opt?.updatedAt ?? new Date()
+ },
include: { forwardedIn: true }
});
+
+ return updated;
};
export const formWithTempStorageFactory = async ({
@@ -521,20 +592,36 @@ export const applicationFactory = async (openIdEnabled?: boolean) => {
export const ecoOrganismeFactory = async ({
siret,
- handleBsdasri = false
+ handle,
+ createAssociatedCompany
}: {
siret?: string;
- handleBsdasri?: boolean;
+ handle?: {
+ handleBsdasri?: boolean;
+ handleBsda?: boolean;
+ handleBsvhu?: boolean;
+ };
+ createAssociatedCompany?: boolean;
}) => {
+ const { handleBsdasri, handleBsda, handleBsvhu } = handle ?? {};
const ecoOrganismeIndex = (await prisma.ecoOrganisme.count()) + 1;
const ecoOrganisme = await prisma.ecoOrganisme.create({
data: {
address: "",
name: `Eco-Organisme ${ecoOrganismeIndex}`,
- siret: siret ?? siretify(ecoOrganismeIndex),
- handleBsdasri
+ siret: siret ?? siretify(),
+ handleBsdasri,
+ handleBsda,
+ handleBsvhu
}
});
+ if (createAssociatedCompany) {
+ // create the related company so sirenify works as expected
+ await companyFactory({
+ siret: ecoOrganisme.siret,
+ name: `Eco-Organisme ${ecoOrganismeIndex}`
+ });
+ }
return ecoOrganisme;
};
@@ -571,3 +658,69 @@ export const transporterReceiptFactory = async ({
return receipt;
};
+
+export const intermediaryReceiptFactory = async ({
+ role = CompanyRole.Broker,
+ number = "el numero",
+ department = "69",
+ company
+}: {
+ role: CompanyRole.Broker | CompanyRole.Trader;
+ number?: string;
+ department?: string;
+ company?: Company;
+}) => {
+ let receipt;
+ if (role === CompanyRole.Broker) {
+ receipt = await prisma.brokerReceipt.create({
+ data: {
+ receiptNumber: number,
+ validityLimit: "2055-01-01T00:00:00.000Z",
+ department: department
+ }
+ });
+ if (!!company) {
+ await prisma.company.update({
+ where: { id: company.id },
+ data: { brokerReceipt: { connect: { id: receipt.id } } }
+ });
+ }
+ } else if (role === CompanyRole.Trader) {
+ receipt = await prisma.traderReceipt.create({
+ data: {
+ receiptNumber: number,
+ validityLimit: "2055-01-01T00:00:00.000Z",
+ department: department
+ }
+ });
+ if (!!company) {
+ await prisma.company.update({
+ where: { id: company.id },
+ data: { traderReceipt: { connect: { id: receipt.id } } }
+ });
+ }
+ }
+ return receipt;
+};
+
+export const bsddFinalOperationFactory = async ({
+ bsddId,
+ opts = {}
+}: {
+ bsddId: string;
+ opts?: Omit<
+ Partial,
+ "initialForm" | "finalForm"
+ >;
+}) => {
+ return prisma.bsddFinalOperation.create({
+ data: {
+ initialForm: { connect: { id: bsddId } },
+ finalForm: { connect: { id: bsddId } },
+ operationCode: "",
+ quantity: 1,
+ noTraceability: false,
+ ...opts
+ }
+ });
+};
diff --git a/back/src/__tests__/testWorkflow.ts b/back/src/__tests__/testWorkflow.ts
index 2678586d3f..4317e28461 100644
--- a/back/src/__tests__/testWorkflow.ts
+++ b/back/src/__tests__/testWorkflow.ts
@@ -19,7 +19,10 @@ async function testWorkflow(workflow: Workflow) {
});
if (workflowCompany.companyTypes.includes("ECO_ORGANISME")) {
// create ecoOrganisme to allow its user to perform api calls
- await ecoOrganismeFactory({ siret: company.siret!, handleBsdasri: true });
+ await ecoOrganismeFactory({
+ siret: company.siret!,
+ handle: { handleBsdasri: true }
+ });
}
if (
workflowCompany.companyTypes.includes("TRANSPORTER") &&
diff --git a/back/src/bsda/__tests__/elastic.integration.ts b/back/src/bsda/__tests__/elastic.integration.ts
index e22f4d3ab0..d5be992465 100644
--- a/back/src/bsda/__tests__/elastic.integration.ts
+++ b/back/src/bsda/__tests__/elastic.integration.ts
@@ -1,10 +1,11 @@
-import { Company } from "@prisma/client";
+import { BsdaStatus, Company, WasteAcceptationStatus } from "@prisma/client";
import { resetDatabase } from "../../../integration-tests/helper";
import { prisma } from "@td/prisma";
import { companyFactory } from "../../__tests__/factories";
import { getBsdaForElastic, toBsdElastic } from "../elastic";
import { BsdElastic } from "../../common/elastic";
import { bsdaFactory } from "./factories";
+import { xDaysAgo } from "../../utils";
describe("toBsdElastic > companies Names & OrgIds", () => {
afterEach(resetDatabase);
@@ -112,4 +113,104 @@ describe("toBsdElastic > companies Names & OrgIds", () => {
expect(elasticBsda.companyOrgIds).toContain(intermediary1.siret);
expect(elasticBsda.companyOrgIds).toContain(intermediary2.siret);
});
+
+ describe("isReturnFor", () => {
+ it.each([
+ WasteAcceptationStatus.REFUSED,
+ WasteAcceptationStatus.PARTIALLY_REFUSED
+ ])(
+ "waste acceptation status is %p > bsda should belong to tab",
+ async destinationReceptionAcceptationStatus => {
+ // Given
+ const transporter = await companyFactory();
+ const bsda = await bsdaFactory({
+ opt: {
+ destinationReceptionDate: new Date(),
+ destinationReceptionAcceptationStatus
+ },
+ transporterOpt: {
+ transporterCompanyName: transporter.name,
+ transporterCompanySiret: transporter.siret,
+ transporterCompanyVatNumber: transporter.vatNumber
+ }
+ });
+
+ // When
+ const bsdaForElastic = await getBsdaForElastic(bsda);
+ const elasticBsda = toBsdElastic(bsdaForElastic);
+
+ // Then
+ expect(elasticBsda.isReturnFor).toContain(transporter?.siret);
+ }
+ );
+
+ it("status is REFUSED > bsda should belong to tab", async () => {
+ // Given
+ const transporter = await companyFactory();
+ const bsda = await bsdaFactory({
+ opt: {
+ destinationReceptionDate: new Date(),
+ status: BsdaStatus.REFUSED
+ },
+ transporterOpt: {
+ transporterCompanyName: transporter.name,
+ transporterCompanySiret: transporter.siret,
+ transporterCompanyVatNumber: transporter.vatNumber
+ }
+ });
+
+ // When
+ const bsdaForElastic = await getBsdaForElastic(bsda);
+ const elasticBsda = toBsdElastic(bsdaForElastic);
+
+ // Then
+ expect(elasticBsda.isReturnFor).toContain(transporter?.siret);
+ });
+
+ it("waste acceptation status is ACCEPTED > bsda should not belong to tab", async () => {
+ // Given
+ const transporter = await companyFactory();
+ const bsda = await bsdaFactory({
+ opt: {
+ destinationReceptionDate: new Date(),
+ destinationReceptionAcceptationStatus: WasteAcceptationStatus.ACCEPTED
+ },
+ transporterOpt: {
+ transporterCompanyName: transporter.name,
+ transporterCompanySiret: transporter.siret,
+ transporterCompanyVatNumber: transporter.vatNumber
+ }
+ });
+
+ // When
+ const bsdaForElastic = await getBsdaForElastic(bsda);
+ const elasticBsda = toBsdElastic(bsdaForElastic);
+
+ // Then
+ expect(elasticBsda.isReturnFor).toStrictEqual([]);
+ });
+
+ it("bsda has been received too long ago > should not belong to tab", async () => {
+ // Given
+ const transporter = await companyFactory();
+ const bsda = await bsdaFactory({
+ opt: {
+ destinationReceptionDate: xDaysAgo(new Date(), 10),
+ destinationReceptionAcceptationStatus: WasteAcceptationStatus.REFUSED
+ },
+ transporterOpt: {
+ transporterCompanyName: transporter.name,
+ transporterCompanySiret: transporter.siret,
+ transporterCompanyVatNumber: transporter.vatNumber
+ }
+ });
+
+ // When
+ const bsdaForElastic = await getBsdaForElastic(bsda);
+ const elasticBsda = toBsdElastic(bsdaForElastic);
+
+ // Then
+ expect(elasticBsda.isReturnFor).toStrictEqual([]);
+ });
+ });
});
diff --git a/back/src/bsda/__tests__/factories.ts b/back/src/bsda/__tests__/factories.ts
index abf3fa10f5..cc2c91c80b 100644
--- a/back/src/bsda/__tests__/factories.ts
+++ b/back/src/bsda/__tests__/factories.ts
@@ -6,7 +6,7 @@ import {
upsertBaseSiret,
userWithCompanyFactory
} from "../../__tests__/factories";
-import { getUserCompanies } from "../../users/database";
+import { getCanAccessDraftOrgIds } from "../utils";
/**
* Permet de créer un BSDA avec des données par défaut et
@@ -83,28 +83,10 @@ export const bsdaFactory = async ({
.filter(Boolean)
: [];
// For drafts, only the owner's sirets that appear on the bsd have access
- const canAccessDraftOrgIds: string[] = [];
- if (created.isDraft) {
- const userCompanies = await getUserCompanies(
- (userId || userAndCompany?.user?.id) as string
- );
- const userOrgIds = userCompanies.map(company => company.orgId);
-
- const bsdaOrgIds = [
- ...intermediariesOrgIds,
- ...transportersOrgIds,
- created.emitterCompanySiret,
- created.ecoOrganismeSiret,
- created.destinationCompanySiret,
- created.destinationOperationNextDestinationCompanySiret,
- created.workerCompanySiret,
- created.brokerCompanySiret
- ].filter(Boolean);
- const userOrgIdsInForm = userOrgIds.filter(orgId =>
- bsdaOrgIds.includes(orgId)
- );
- canAccessDraftOrgIds.push(...userOrgIdsInForm);
- }
+ const canAccessDraftOrgIds = await getCanAccessDraftOrgIds(
+ created,
+ (userId || userAndCompany?.user?.id) as string
+ );
return prisma.bsda.update({
where: { id: created.id },
@@ -140,6 +122,27 @@ export const bsdaTransporterFactory = async ({
});
};
+export const bsdaFinalOperationFactory = async ({
+ bsdaId,
+ opts = {}
+}: {
+ bsdaId: string;
+ opts?: Omit<
+ Partial,
+ "initialBsda" | "finalBsda"
+ >;
+}) => {
+ return prisma.bsdaFinalOperation.create({
+ data: {
+ initialBsda: { connect: { id: bsdaId } },
+ finalBsda: { connect: { id: bsdaId } },
+ operationCode: "",
+ quantity: 1,
+ ...opts
+ }
+ });
+};
+
const bsdaTransporterData: Omit<
Prisma.BsdaTransporterCreateWithoutBsdaInput,
"number"
diff --git a/back/src/bsda/database.ts b/back/src/bsda/database.ts
index cd1e10c7da..3c741a2ca8 100644
--- a/back/src/bsda/database.ts
+++ b/back/src/bsda/database.ts
@@ -118,6 +118,15 @@ export function getFirstTransporterSync(bsda: {
return firstTransporter ?? null;
}
+export function getLastTransporterSync(bsda: {
+ transporters: BsdaTransporter[] | null;
+}): BsdaTransporter | null {
+ const transporters = getTransportersSync(bsda);
+ const greatestNumber = Math.max(...transporters.map(t => t.number));
+ const lastTransporter = transporters.find(t => t.number === greatestNumber);
+ return lastTransporter ?? null;
+}
+
export function getNthTransporterSync(
bsda: BsdaWithTransporters,
n: number
diff --git a/back/src/bsda/elastic.ts b/back/src/bsda/elastic.ts
index 250fbe6e0a..a96442758e 100644
--- a/back/src/bsda/elastic.ts
+++ b/back/src/bsda/elastic.ts
@@ -1,4 +1,10 @@
-import { Bsda, BsdaStatus, BsdaTransporter, BsdType } from "@prisma/client";
+import {
+ Bsda,
+ BsdaStatus,
+ BsdaTransporter,
+ BsdType,
+ WasteAcceptationStatus
+} from "@prisma/client";
import { getTransporterCompanyOrgId } from "@td/constants";
import { BsdElastic, indexBsd, transportPlateFilter } from "../common/elastic";
import { buildAddress } from "../companies/sirene/utils";
@@ -20,10 +26,13 @@ import { prisma } from "@td/prisma";
import { getRevisionOrgIds } from "../common/elasticHelpers";
import {
getFirstTransporterSync,
+ getLastTransporterSync,
getNextTransporterSync,
getTransportersSync
} from "./database";
import { getBsdaSubType } from "../common/subTypes";
+import { isDefined } from "../common/helpers";
+import { xDaysAgo } from "../utils";
export type BsdaForElastic = Bsda &
BsdaWithTransporters &
@@ -380,6 +389,7 @@ export function toBsdElastic(bsda: BsdaForElastic): BsdElastic {
destinationOperationDate: bsda.destinationOperationDate?.getTime(),
...where,
...getBsdaRevisionOrgIds(bsda),
+ ...getBsdaReturnOrgIds(bsda),
revisionRequests: bsda.bsdaRevisionRequests,
sirets: Object.values(where).flat(),
...getRegistryFields(bsda),
@@ -428,3 +438,36 @@ export function getBsdaRevisionOrgIds(
): Pick {
return getRevisionOrgIds(bsda.bsdaRevisionRequests);
}
+
+/**
+ * BSDA belongs to isReturnFor tab if:
+ * - waste has been received in the last 48 hours
+ * - waste hasn't been fully accepted
+ */
+export const belongsToIsReturnForTab = (bsda: BsdaForElastic) => {
+ const hasBeenReceivedLately =
+ isDefined(bsda.destinationReceptionDate) &&
+ bsda.destinationReceptionDate! > xDaysAgo(new Date(), 2);
+
+ if (!hasBeenReceivedLately) return false;
+
+ const hasNotBeenFullyAccepted =
+ bsda.status === BsdaStatus.REFUSED ||
+ bsda.destinationReceptionAcceptationStatus !==
+ WasteAcceptationStatus.ACCEPTED;
+
+ return hasNotBeenFullyAccepted;
+};
+
+function getBsdaReturnOrgIds(bsda: BsdaForElastic): { isReturnFor: string[] } {
+ // Return tab
+ if (belongsToIsReturnForTab(bsda)) {
+ const lastTransporter = getLastTransporterSync(bsda);
+
+ return {
+ isReturnFor: [lastTransporter?.transporterCompanySiret].filter(Boolean)
+ };
+ }
+
+ return { isReturnFor: [] };
+}
diff --git a/back/src/bsda/permissions.ts b/back/src/bsda/permissions.ts
index 9a26dcd808..9bb08dd886 100644
--- a/back/src/bsda/permissions.ts
+++ b/back/src/bsda/permissions.ts
@@ -298,9 +298,10 @@ export async function checkCanDelete(user: User, bsda: BsdaWithTransporters) {
? [bsda.emitterCompanySiret]
: [];
- const errorMsg = bsda.isDraft
- ? "Vous n'êtes pas autorisé à supprimer ce bordereau."
- : "Seuls les bordereaux en brouillon ou n'ayant pas encore été signés peuvent être supprimés";
+ const errorMsg =
+ bsda.status === BsdaStatus.INITIAL
+ ? "Vous n'êtes pas autorisé à supprimer ce bordereau."
+ : "Seuls les bordereaux en brouillon ou n'ayant pas encore été signés peuvent être supprimés";
return checkUserPermissions(
user,
authorizedOrgIds,
diff --git a/back/src/bsda/registry.ts b/back/src/bsda/registry.ts
index a9de2afbd3..16b1e26836 100644
--- a/back/src/bsda/registry.ts
+++ b/back/src/bsda/registry.ts
@@ -417,7 +417,7 @@ export function toIncomingWaste(bsda: RegistryBsda): Required {
traderRecepisseNumber: null,
brokerCompanyName: bsda.brokerCompanyName,
brokerCompanySiret: bsda.brokerCompanySiret,
- brokerRecepisseNumber: null,
+ brokerRecepisseNumber: bsda.brokerRecepisseNumber,
emitterCompanyMail: bsda.emitterCompanyMail,
...getOperationData(bsda),
nextDestinationProcessingOperation:
diff --git a/back/src/bsda/repository/bsda/create.ts b/back/src/bsda/repository/bsda/create.ts
index 4322037ae9..3701b64672 100644
--- a/back/src/bsda/repository/bsda/create.ts
+++ b/back/src/bsda/repository/bsda/create.ts
@@ -6,7 +6,7 @@ import {
import { enqueueCreatedBsdToIndex } from "../../../queue/producers/elastic";
import { bsdaEventTypes } from "./eventTypes";
import { BsdaWithTransporters } from "../../types";
-import { getUserCompanies } from "../../../users/database";
+import { getCanAccessDraftOrgIds } from "../../utils";
export type CreateBsdaFn = (
data: Prisma.BsdaCreateInput,
@@ -52,11 +52,7 @@ export function buildCreateBsda(deps: RepositoryFnDeps): CreateBsdaFn {
)
);
}
- const intermediariesOrgIds: string[] = bsda.intermediaries
- ? bsda.intermediaries
- .flatMap(intermediary => [intermediary.siret, intermediary.vatNumber])
- .filter(Boolean)
- : [];
+
const transportersOrgIds: string[] = bsda.transporters
? bsda.transporters
.flatMap(t => [
@@ -66,32 +62,13 @@ export function buildCreateBsda(deps: RepositoryFnDeps): CreateBsdaFn {
.filter(Boolean)
: [];
// For drafts, only the owner's sirets that appear on the bsd have access
- const canAccessDraftOrgIds: string[] = [];
- if (bsda.isDraft) {
- const userCompanies = await getUserCompanies(user.id);
- const userOrgIds = userCompanies.map(company => company.orgId);
- const bsdaOrgIds = [
- ...intermediariesOrgIds,
- ...transportersOrgIds,
- bsda.emitterCompanySiret,
- bsda.ecoOrganismeSiret,
- bsda.destinationCompanySiret,
- bsda.destinationOperationNextDestinationCompanySiret,
- bsda.workerCompanySiret,
- bsda.brokerCompanySiret
- ].filter(Boolean);
- const userOrgIdsInForm = userOrgIds.filter(orgId =>
- bsdaOrgIds.includes(orgId)
- );
- canAccessDraftOrgIds.push(...userOrgIdsInForm);
- }
+ const canAccessDraftOrgIds = await getCanAccessDraftOrgIds(bsda, user.id);
const updatedBsda = await prisma.bsda.update({
where: { id: bsda.id },
data: {
...(canAccessDraftOrgIds.length ? { canAccessDraftOrgIds } : {}),
- ...(transportersOrgIds.length ? { transportersOrgIds } : {}),
- transportersOrgIds
+ ...(transportersOrgIds.length ? { transportersOrgIds } : {})
},
include: {
grouping: { select: { id: true } },
diff --git a/back/src/bsda/resolvers/mutations/sign.ts b/back/src/bsda/resolvers/mutations/sign.ts
index 4b461fd352..df63b4c976 100644
--- a/back/src/bsda/resolvers/mutations/sign.ts
+++ b/back/src/bsda/resolvers/mutations/sign.ts
@@ -3,6 +3,7 @@ import {
BsdaStatus,
BsdaType,
Prisma,
+ UserNotification,
WasteAcceptationStatus
} from "@prisma/client";
@@ -41,6 +42,7 @@ import { parseBsdaAsync } from "../../validation";
import { prismaToZodBsda } from "../../validation/helpers";
import { AlreadySignedError } from "../../../bsvhu/errors";
import { operationHook } from "../../operationHook";
+import { getNotificationSubscribers } from "../../../users/notifications";
const signBsda: MutationResolvers["signBsda"] = async (
_,
@@ -391,36 +393,40 @@ async function sendAlertIfFollowingBsdaChangedPlannedDestination(bsda: Bsda) {
}
const previousBsdas = await getBsdaHistory(bsda);
+
for (const previousBsda of previousBsdas) {
if (
previousBsda.destinationOperationNextDestinationCompanySiret &&
previousBsda.destinationOperationNextDestinationCompanySiret !==
bsda.destinationCompanySiret
) {
- const mail = renderMail(finalDestinationModified, {
- to: [
- {
- email: previousBsda.emitterCompanyMail!,
- name: previousBsda.emitterCompanyName!
- }
- ],
- variables: {
- id: previousBsda.id,
- emitter: {
- siret: bsda.emitterCompanySiret!,
- name: bsda.emitterCompanyName!
- },
- destination: {
- siret: bsda.destinationCompanySiret!,
- name: bsda.destinationCompanyName!
- },
- plannedDestination: {
- siret: previousBsda.destinationOperationNextDestinationCompanySiret,
- name: previousBsda.destinationOperationNextDestinationCompanyName!
+ const subscribers = await getNotificationSubscribers(
+ UserNotification.BSDA_FINAL_DESTINATION_UPDATE,
+ [previousBsda.emitterCompanySiret].filter(Boolean)
+ );
+
+ if (subscribers.length) {
+ const mail = renderMail(finalDestinationModified, {
+ to: subscribers,
+ variables: {
+ id: previousBsda.id,
+ emitter: {
+ siret: bsda.emitterCompanySiret!,
+ name: bsda.emitterCompanyName!
+ },
+ destination: {
+ siret: bsda.destinationCompanySiret!,
+ name: bsda.destinationCompanyName!
+ },
+ plannedDestination: {
+ siret:
+ previousBsda.destinationOperationNextDestinationCompanySiret,
+ name: previousBsda.destinationOperationNextDestinationCompanyName!
+ }
}
- }
- });
- sendMail(mail);
+ });
+ sendMail(mail);
+ }
}
}
}
diff --git a/back/src/bsda/utils.ts b/back/src/bsda/utils.ts
new file mode 100644
index 0000000000..c9b4162867
--- /dev/null
+++ b/back/src/bsda/utils.ts
@@ -0,0 +1,42 @@
+import { getUserCompanies } from "../users/database";
+import { BsdaWithIntermediaries, BsdaWithTransporters } from "./types";
+
+export const getCanAccessDraftOrgIds = async (
+ bsda: BsdaWithIntermediaries & BsdaWithTransporters,
+ userId: string
+): Promise => {
+ const intermediariesOrgIds: string[] = bsda.intermediaries
+ ? bsda.intermediaries
+ .flatMap(intermediary => [intermediary.siret, intermediary.vatNumber])
+ .filter(Boolean)
+ : [];
+ const transportersOrgIds: string[] = bsda.transporters
+ ? bsda.transporters
+ .flatMap(t => [
+ t.transporterCompanySiret,
+ t.transporterCompanyVatNumber
+ ])
+ .filter(Boolean)
+ : [];
+ // For drafts, only the owner's sirets that appear on the bsd have access
+ const canAccessDraftOrgIds: string[] = [];
+ if (bsda.isDraft) {
+ const userCompanies = await getUserCompanies(userId);
+ const userOrgIds = userCompanies.map(company => company.orgId);
+ const bsdaOrgIds = [
+ ...intermediariesOrgIds,
+ ...transportersOrgIds,
+ bsda.emitterCompanySiret,
+ bsda.ecoOrganismeSiret,
+ bsda.destinationCompanySiret,
+ bsda.destinationOperationNextDestinationCompanySiret,
+ bsda.workerCompanySiret,
+ bsda.brokerCompanySiret
+ ].filter(Boolean);
+ const userOrgIdsInForm = userOrgIds.filter(orgId =>
+ bsdaOrgIds.includes(orgId)
+ );
+ canAccessDraftOrgIds.push(...userOrgIdsInForm);
+ }
+ return canAccessDraftOrgIds;
+};
diff --git a/back/src/bsda/validation/recipify.ts b/back/src/bsda/validation/recipify.ts
new file mode 100644
index 0000000000..d7696f47c7
--- /dev/null
+++ b/back/src/bsda/validation/recipify.ts
@@ -0,0 +1,65 @@
+import { getTransporterCompanyOrgId } from "@td/constants";
+import { ParsedZodBsda } from "./schema";
+import { CompanyRole } from "../../common/validation/zod/schema";
+import {
+ buildRecipify,
+ RecipifyInputAccessor
+} from "../../common/validation/recipify";
+
+const recipifyBsdaAccessors = (
+ bsd: ParsedZodBsda,
+ // Tranformations should not be run on sealed fields
+ sealedFields: string[]
+): RecipifyInputAccessor[] => [
+ ...(bsd.transporters ?? []).map(
+ (_, idx) =>
+ ({
+ role: CompanyRole.Transporter,
+ skip: !!bsd.transporters![idx].transporterTransportSignatureDate,
+ orgIdGetter: () => {
+ const orgId = getTransporterCompanyOrgId({
+ transporterCompanySiret:
+ bsd.transporters![idx].transporterCompanySiret ?? null,
+ transporterCompanyVatNumber:
+ bsd.transporters![idx].transporterCompanyVatNumber ?? null
+ });
+ return orgId ?? null;
+ },
+ setter: async (bsda: ParsedZodBsda, receipt) => {
+ const transporter = bsda.transporters![idx];
+ if (transporter.transporterRecepisseIsExempted) {
+ transporter.transporterRecepisseNumber = null;
+ transporter.transporterRecepisseValidityLimit = null;
+ transporter.transporterRecepisseDepartment = null;
+ } else {
+ transporter.transporterRecepisseNumber =
+ receipt?.receiptNumber ?? null;
+ transporter.transporterRecepisseValidityLimit =
+ receipt?.validityLimit ?? null;
+ transporter.transporterRecepisseDepartment =
+ receipt?.department ?? null;
+ }
+ }
+ } as RecipifyInputAccessor)
+ ),
+ {
+ role: CompanyRole.Broker,
+ skip: sealedFields.includes("brokerRecepisseNumber"),
+ orgIdGetter: () => {
+ return bsd.brokerCompanySiret ?? null;
+ },
+ setter: async (bsda: ParsedZodBsda, receipt) => {
+ if (!bsda.brokerRecepisseNumber && receipt?.receiptNumber) {
+ bsda.brokerRecepisseNumber = receipt.receiptNumber;
+ }
+ if (!bsda.brokerRecepisseValidityLimit && receipt?.validityLimit) {
+ bsda.brokerRecepisseValidityLimit = receipt.validityLimit;
+ }
+ if (!bsda.brokerRecepisseDepartment && receipt?.department) {
+ bsda.brokerRecepisseDepartment = receipt.department;
+ }
+ }
+ }
+];
+
+export const recipifyBsda = buildRecipify(recipifyBsdaAccessors);
diff --git a/back/src/bsda/validation/refinements.ts b/back/src/bsda/validation/refinements.ts
index cc0364f0d1..027a1f1ed1 100644
--- a/back/src/bsda/validation/refinements.ts
+++ b/back/src/bsda/validation/refinements.ts
@@ -15,6 +15,7 @@ import {
Bsda,
BsdaStatus,
BsdaType,
+ BsdType,
Company,
CompanyType
} from "@prisma/client";
@@ -26,11 +27,13 @@ import { prisma } from "@td/prisma";
import { isWorker } from "../../companies/validation";
import {
isDestinationRefinement,
- isNotDormantRefinement,
+ isEcoOrganismeRefinement,
+ isEmitterNotDormantRefinement,
isRegisteredVatNumberRefinement,
isTransporterRefinement,
refineSiretAndGetCompany
} from "../../common/validation/zod/refinement";
+import { CompanyRole } from "../../common/validation/zod/schema";
export const checkOperationIsAfterReception: Refinement = (
bsda,
@@ -259,36 +262,6 @@ export const checkRequiredFields: (
};
};
-async function refineAndGetEcoOrganisme(siret: string | null | undefined, ctx) {
- if (!siret) return null;
- const ecoOrganisme = await prisma.ecoOrganisme.findUnique({
- where: { siret }
- });
-
- if (ecoOrganisme === null) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: `L'éco-organisme avec le SIRET ${siret} n'est pas référencé sur Trackdéchets`
- });
- }
-
- return ecoOrganisme;
-}
-
-async function isBsdaEcoOrganismeRefinement(
- siret: string | null | undefined,
- ctx: RefinementCtx
-) {
- const ecoOrganisme = await refineAndGetEcoOrganisme(siret, ctx);
-
- if (ecoOrganisme && !ecoOrganisme?.handleBsda) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message: `L'éco-organisme avec le SIRET ${siret} n'est pas autorisé à apparaitre sur un BSDA`
- });
- }
-}
-
async function checkEmitterIsNotEcoOrganisme(
siret: string | null | undefined,
ctx: RefinementCtx
@@ -327,17 +300,19 @@ export const checkCompanies: Refinement = async (
);
};
- await isNotDormantRefinement(bsda.emitterCompanySiret, zodContext);
+ await isEmitterNotDormantRefinement(bsda.emitterCompanySiret, zodContext);
await isDestinationRefinement(
bsda.destinationCompanySiret,
zodContext,
"DESTINATION",
+ CompanyRole.Destination,
isBsdaDestinationExemptFromVerification
);
await isDestinationRefinement(
bsda.destinationOperationNextDestinationCompanySiret,
zodContext,
"DESTINATION",
+ CompanyRole.DestinationOperationNextDestination,
isBsdaDestinationExemptFromVerification
);
for (const transporter of bsda.transporters ?? []) {
@@ -355,7 +330,11 @@ export const checkCompanies: Refinement = async (
);
}
await isWorkerRefinement(bsda.workerCompanySiret, zodContext);
- await isBsdaEcoOrganismeRefinement(bsda.ecoOrganismeSiret, zodContext);
+ await isEcoOrganismeRefinement(
+ bsda.ecoOrganismeSiret,
+ BsdType.BSDA,
+ zodContext
+ );
await checkEmitterIsNotEcoOrganisme(bsda.emitterCompanySiret, zodContext);
};
diff --git a/back/src/bsda/validation/rules.ts b/back/src/bsda/validation/rules.ts
index 336610be7e..ea32fdbd00 100644
--- a/back/src/bsda/validation/rules.ts
+++ b/back/src/bsda/validation/rules.ts
@@ -725,8 +725,7 @@ export const bsdaEditionRules: BsdaEditionRules = {
sealed: { from: "OPERATION" }
},
brokerRecepisseValidityLimit: {
- readableFieldName:
- "la date de validité de la certification de l'entreprise de travaux",
+ readableFieldName: "la date de validité du récépissé du courtier",
sealed: { from: "OPERATION" }
},
wasteCode: {
diff --git a/back/src/bsda/validation/schema.ts b/back/src/bsda/validation/schema.ts
index ddeb053516..143821b0fe 100644
--- a/back/src/bsda/validation/schema.ts
+++ b/back/src/bsda/validation/schema.ts
@@ -32,10 +32,10 @@ import {
fillIntermediariesOrgIds,
fillWasteConsistenceWhenForwarding,
emptyWorkerCertificationWhenWorkerIsDisabled,
- updateTransporterRecepisse
+ updateTransporterRecepisse,
+ runTransformers
} from "./transformers";
-import { sirenifyBsda, sirenifyBsdaTransporter } from "./sirenify";
-import { updateTransportersRecepisse } from "../../common/validation/zod/transformers";
+import { sirenifyBsdaTransporter } from "./sirenify";
import {
CompanyRole,
foreignVatNumberSchema,
@@ -291,8 +291,7 @@ export const contextualSchemaAsync = (context: BsdaValidationContext) => {
? // Transformations asynchrones qui ne sont pas
// `enableCompletionTransformers=false`;
transformedSyncSchema
- .transform(sirenifyBsda(context))
- .transform(updateTransportersRecepisse)
+ .transform((bsda: ParsedZodBsda) => runTransformers(bsda, context))
.transform(fillWasteConsistenceWhenForwarding)
: transformedSyncSchema;
diff --git a/back/src/bsda/validation/sirenify.ts b/back/src/bsda/validation/sirenify.ts
index 2711bb483f..68552c9092 100644
--- a/back/src/bsda/validation/sirenify.ts
+++ b/back/src/bsda/validation/sirenify.ts
@@ -1,21 +1,19 @@
import { ParsedZodBsda, ParsedZodBsdaTransporter } from "./schema";
import { CompanyInput } from "../../generated/graphql/types";
-import { nextBuildSirenify } from "../../companies/sirenify";
-import { getSealedFields } from "./rules";
import {
- BsdaValidationContext,
- ZodBsdaTransformer,
- ZodBsdaTransporterTransformer
-} from "./types";
+ nextBuildSirenify,
+ NextCompanyInputAccessor
+} from "../../companies/sirenify";
+import { ZodBsdaTransporterTransformer } from "./types";
const sirenifyBsdaAccessors = (
bsda: ParsedZodBsda,
sealedFields: string[] // Tranformations should not be run on sealed fields
-) => [
+): NextCompanyInputAccessor[] => [
{
siret: bsda?.emitterCompanySiret,
skip: sealedFields.includes("emitterCompanySiret"),
- setter: (input, companyInput: CompanyInput) => {
+ setter: (input, companyInput) => {
input.emitterCompanyName = companyInput.name;
input.emitterCompanyAddress = companyInput.address;
}
@@ -23,7 +21,7 @@ const sirenifyBsdaAccessors = (
{
siret: bsda?.destinationCompanySiret,
skip: sealedFields.includes("destinationCompanySiret"),
- setter: (input, companyInput: CompanyInput) => {
+ setter: (input, companyInput) => {
input.destinationCompanyName = companyInput.name;
input.destinationCompanyAddress = companyInput.address;
}
@@ -31,7 +29,7 @@ const sirenifyBsdaAccessors = (
{
siret: bsda?.workerCompanySiret,
skip: sealedFields.includes("workerCompanySiret"),
- setter: (input, companyInput: CompanyInput) => {
+ setter: (input, companyInput) => {
input.workerCompanyName = companyInput.name;
input.workerCompanyAddress = companyInput.address;
}
@@ -39,43 +37,58 @@ const sirenifyBsdaAccessors = (
{
siret: bsda?.brokerCompanySiret,
skip: sealedFields.includes("brokerCompanySiret"),
- setter: (input, companyInput: CompanyInput) => {
+ setter: (input, companyInput) => {
input.brokerCompanyName = companyInput.name;
input.brokerCompanyAddress = companyInput.address;
}
},
- ...(bsda.intermediaries ?? []).map((_, idx) => ({
- siret: bsda.intermediaries![idx].siret,
- skip: sealedFields.includes("intermediaries"),
- setter: (input, companyInput: CompanyInput) => {
- const intermediary = input.intermediaries[idx];
- intermediary.name = companyInput.name;
- intermediary.address = companyInput.address;
- }
- })),
- ...(bsda.transporters ?? []).map((_, idx) => ({
- siret: bsda.transporters![idx].transporterCompanySiret,
- // FIXME skip conditionnaly based on transporter signatures
- skip: false,
- setter: (input, companyInput: CompanyInput) => {
- const transporter = input.transporters[idx];
- transporter.transporterCompanyName = companyInput.name;
- transporter.transporterCompanyAddress = companyInput.address;
+ {
+ siret: bsda?.ecoOrganismeSiret,
+ skip: sealedFields.includes("ecoOrganismeSiret"),
+ setter: (input, companyInput) => {
+ if (companyInput.name) {
+ input.ecoOrganismeName = companyInput.name;
+ }
}
- }))
+ },
+ ...(bsda.intermediaries ?? []).map(
+ (_, idx) =>
+ ({
+ siret: bsda.intermediaries![idx].siret,
+ skip: sealedFields.includes("intermediaries"),
+ setter: (input, companyInput) => {
+ const intermediary = input.intermediaries![idx];
+ if (companyInput.name) {
+ intermediary!.name = companyInput.name;
+ }
+ if (companyInput.address) {
+ intermediary!.address = companyInput.address;
+ }
+ }
+ } as NextCompanyInputAccessor)
+ ),
+ ...(bsda.transporters ?? []).map(
+ (_, idx) =>
+ ({
+ siret: bsda.transporters![idx].transporterCompanySiret,
+ // FIXME skip conditionnaly based on transporter signatures
+ skip: false,
+ setter: (input, companyInput) => {
+ const transporter = input.transporters![idx];
+ if (companyInput.name) {
+ transporter!.transporterCompanyName = companyInput.name;
+ }
+ if (companyInput.address) {
+ transporter!.transporterCompanyAddress = companyInput.address;
+ }
+ }
+ } as NextCompanyInputAccessor)
+ )
];
-export const sirenifyBsda: (
- context: BsdaValidationContext
-) => ZodBsdaTransformer = context => {
- return async bsda => {
- const sealedFields = await getSealedFields(bsda, context);
- return nextBuildSirenify(sirenifyBsdaAccessors)(
- bsda,
- sealedFields
- );
- };
-};
+export const sirenifyBsda = nextBuildSirenify(
+ sirenifyBsdaAccessors
+);
const sirenifyBsdaTransporterAccessors = (
bsdaTransporter: ParsedZodBsdaTransporter
diff --git a/back/src/bsda/validation/transformers.ts b/back/src/bsda/validation/transformers.ts
index 61e244cde3..ec3ba5ae66 100644
--- a/back/src/bsda/validation/transformers.ts
+++ b/back/src/bsda/validation/transformers.ts
@@ -1,6 +1,27 @@
import { prisma } from "@td/prisma";
-import { ZodBsdaTransformer, ZodBsdaTransporterTransformer } from "./types";
+import {
+ BsdaValidationContext,
+ ZodBsdaTransformer,
+ ZodBsdaTransporterTransformer
+} from "./types";
import { recipifyTransporter } from "../../common/validation/zod/transformers";
+import { ParsedZodBsda } from "./schema";
+import { sirenifyBsda } from "./sirenify";
+import { recipifyBsda } from "./recipify";
+import { getSealedFields } from "./rules";
+
+export const runTransformers = async (
+ bsda: ParsedZodBsda,
+ context: BsdaValidationContext
+): Promise => {
+ const transformers = [sirenifyBsda, recipifyBsda];
+ const sealedFields = await getSealedFields(bsda, context);
+
+ for (const transformer of transformers) {
+ bsda = await transformer(bsda, sealedFields);
+ }
+ return bsda;
+};
export const fillIntermediariesOrgIds: ZodBsdaTransformer = bsda => {
bsda.intermediariesOrgIds = bsda.intermediaries
diff --git a/back/src/bsdasris/__tests__/elastic.integration.ts b/back/src/bsdasris/__tests__/elastic.integration.ts
index 800aca9c2c..cd89fb5504 100644
--- a/back/src/bsdasris/__tests__/elastic.integration.ts
+++ b/back/src/bsdasris/__tests__/elastic.integration.ts
@@ -3,7 +3,8 @@ import { companyFactory } from "../../__tests__/factories";
import { getBsdasriForElastic, toBsdElastic } from "../elastic";
import { BsdElastic } from "../../common/elastic";
import { bsdasriFactory } from "./factories";
-import { Company } from "@prisma/client";
+import { BsdasriStatus, Company, WasteAcceptationStatus } from "@prisma/client";
+import { xDaysAgo } from "../../utils";
describe("toBsdElastic > companies Names & OrgIds", () => {
afterEach(resetDatabase);
@@ -60,4 +61,100 @@ describe("toBsdElastic > companies Names & OrgIds", () => {
expect(elasticBsdasri.companyOrgIds).toContain(destination.siret);
expect(elasticBsdasri.companyOrgIds).toContain(ecoOrganisme.siret);
});
+
+ describe("isReturnFor", () => {
+ it.each([
+ WasteAcceptationStatus.REFUSED,
+ WasteAcceptationStatus.PARTIALLY_REFUSED
+ ])(
+ "waste acceptation status is %p > bsvhu should belong to tab",
+ async destinationReceptionAcceptationStatus => {
+ // Given
+ const transporter = await companyFactory();
+ const bsdasri = await bsdasriFactory({
+ opt: {
+ emitterCompanyName: emitter.name,
+ emitterCompanySiret: emitter.siret,
+ transporterCompanyName: transporter.name,
+ transporterCompanySiret: transporter.siret,
+ destinationReceptionDate: new Date(),
+ destinationReceptionAcceptationStatus
+ }
+ });
+
+ // When
+ const bsdasriForElastic = await getBsdasriForElastic(bsdasri);
+ const { isReturnFor } = toBsdElastic(bsdasriForElastic);
+
+ // Then
+ expect(isReturnFor).toContain(transporter.siret);
+ }
+ );
+
+ it("status is REFUSED > bsvhu should belong to tab", async () => {
+ // Given
+ const transporter = await companyFactory();
+ const bsdasri = await bsdasriFactory({
+ opt: {
+ emitterCompanyName: emitter.name,
+ emitterCompanySiret: emitter.siret,
+ transporterCompanyName: transporter.name,
+ transporterCompanySiret: transporter.siret,
+ destinationReceptionDate: new Date(),
+ status: BsdasriStatus.REFUSED
+ }
+ });
+
+ // When
+ const bsdasriForElastic = await getBsdasriForElastic(bsdasri);
+ const { isReturnFor } = toBsdElastic(bsdasriForElastic);
+
+ // Then
+ expect(isReturnFor).toContain(transporter.siret);
+ });
+
+ it("waste acceptation status is ACCEPTED > bsvhu should not belong to tab", async () => {
+ // Given
+ const transporter = await companyFactory();
+ const bsdasri = await bsdasriFactory({
+ opt: {
+ emitterCompanyName: emitter.name,
+ emitterCompanySiret: emitter.siret,
+ transporterCompanyName: transporter.name,
+ transporterCompanySiret: transporter.siret,
+ destinationReceptionDate: new Date(),
+ destinationReceptionAcceptationStatus: WasteAcceptationStatus.ACCEPTED
+ }
+ });
+
+ // When
+ const bsdasriForElastic = await getBsdasriForElastic(bsdasri);
+ const { isReturnFor } = toBsdElastic(bsdasriForElastic);
+
+ // Then
+ expect(isReturnFor).toStrictEqual([]);
+ });
+
+ it("bsda has been received too long ago > should not belong to tab", async () => {
+ // Given
+ const transporter = await companyFactory();
+ const bsdasri = await bsdasriFactory({
+ opt: {
+ emitterCompanyName: emitter.name,
+ emitterCompanySiret: emitter.siret,
+ transporterCompanyName: transporter.name,
+ transporterCompanySiret: transporter.siret,
+ destinationReceptionDate: xDaysAgo(new Date(), 10),
+ destinationReceptionAcceptationStatus: WasteAcceptationStatus.REFUSED
+ }
+ });
+
+ // When
+ const bsdasriForElastic = await getBsdasriForElastic(bsdasri);
+ const { isReturnFor } = toBsdElastic(bsdasriForElastic);
+
+ // Then
+ expect(isReturnFor).toStrictEqual([]);
+ });
+ });
});
diff --git a/back/src/bsdasris/__tests__/factories.ts b/back/src/bsdasris/__tests__/factories.ts
index e22567b281..56e53e4f0f 100644
--- a/back/src/bsdasris/__tests__/factories.ts
+++ b/back/src/bsdasris/__tests__/factories.ts
@@ -65,6 +65,27 @@ export const bsdasriFactory = async ({
return created;
};
+export const bsdasriFinalOperationFactory = async ({
+ bsdasriId,
+ opts = {}
+}: {
+ bsdasriId: string;
+ opts?: Omit<
+ Partial,
+ "initialBsdasri" | "finalBsdasri"
+ >;
+}) => {
+ return prisma.bsdasriFinalOperation.create({
+ data: {
+ initialBsdasri: { connect: { id: bsdasriId } },
+ finalBsdasri: { connect: { id: bsdasriId } },
+ operationCode: "",
+ quantity: 1,
+ ...opts
+ }
+ });
+};
+
export const initialData = company => ({
emitterCompanySiret: company.siret,
emitterCompanyName: company.name,
diff --git a/back/src/bsdasris/elastic.ts b/back/src/bsdasris/elastic.ts
index 7670a7fb9f..73cf1a07f1 100644
--- a/back/src/bsdasris/elastic.ts
+++ b/back/src/bsdasris/elastic.ts
@@ -1,4 +1,10 @@
-import { BsdasriStatus, Bsdasri, BsdasriType, BsdType } from "@prisma/client";
+import {
+ BsdasriStatus,
+ Bsdasri,
+ BsdasriType,
+ BsdType,
+ WasteAcceptationStatus
+} from "@prisma/client";
import { BsdElastic, indexBsd, transportPlateFilter } from "../common/elastic";
import { GraphQLContext } from "../types";
import { getRegistryFields } from "./registry";
@@ -15,6 +21,8 @@ import {
import { prisma } from "@td/prisma";
import { getRevisionOrgIds } from "../common/elasticHelpers";
import { getBsdasriSubType } from "../common/subTypes";
+import { isDefined } from "../common/helpers";
+import { xDaysAgo } from "../utils";
export type BsdasriForElastic = Bsdasri &
BsdasriWithGrouping &
@@ -223,6 +231,7 @@ export function toBsdElastic(bsdasri: BsdasriForElastic): BsdElastic {
...where,
...getBsdasriRevisionOrgIds(bsdasri),
+ ...getBsdasriReturnOrgIds(bsdasri),
revisionRequests: bsdasri.bsdasriRevisionRequests,
sirets: Object.values(where).flat(),
@@ -260,3 +269,34 @@ export function getBsdasriRevisionOrgIds(
): Pick {
return getRevisionOrgIds(bsdasri.bsdasriRevisionRequests);
}
+
+/**
+ * BSDASRI belongs to isReturnFor tab if:
+ * - waste has been received in the last 48 hours
+ * - waste hasn't been fully accepted at reception
+ */
+export const belongsToIsReturnForTab = (bsdasri: Bsdasri) => {
+ const hasBeenReceivedLately =
+ isDefined(bsdasri.destinationReceptionDate) &&
+ bsdasri.destinationReceptionDate! > xDaysAgo(new Date(), 2);
+
+ if (!hasBeenReceivedLately) return false;
+
+ const hasNotBeenFullyAccepted =
+ bsdasri.status === BsdasriStatus.REFUSED ||
+ bsdasri.destinationReceptionAcceptationStatus !==
+ WasteAcceptationStatus.ACCEPTED;
+
+ return hasNotBeenFullyAccepted;
+};
+
+function getBsdasriReturnOrgIds(bsdasri: Bsdasri): { isReturnFor: string[] } {
+ // Return tab
+ if (belongsToIsReturnForTab(bsdasri)) {
+ return {
+ isReturnFor: [bsdasri.transporterCompanySiret].filter(Boolean)
+ };
+ }
+
+ return { isReturnFor: [] };
+}
diff --git a/back/src/bsdasris/recipify.ts b/back/src/bsdasris/recipify.ts
index 099cbda196..16444febb8 100644
--- a/back/src/bsdasris/recipify.ts
+++ b/back/src/bsdasris/recipify.ts
@@ -3,10 +3,19 @@ import {
BsdasriRecepisseInput
} from "../generated/graphql/types";
import { recipifyGeneric } from "../companies/recipify";
-import {
- autocompletedRecepisse,
- genericGetter
-} from "../common/validation/recipify";
+
+export const autocompletedRecepisse = (
+ input: BsdasriInput,
+ recepisseInput: BsdasriRecepisseInput
+) => ({
+ ...input.transporter?.recepisse,
+ ...recepisseInput
+});
+
+const genericGetter = (input: BsdasriInput) => () =>
+ input?.transporter?.recepisse?.isExempted !== true
+ ? input?.transporter?.company
+ : null;
const dasriAccessors = (input: BsdasriInput) => [
{
diff --git a/back/src/bsdasris/resolvers/mutations/__tests__/createBsdasriEcoOrganisme.integration.ts b/back/src/bsdasris/resolvers/mutations/__tests__/createBsdasriEcoOrganisme.integration.ts
index 9a5493f98e..163261ffa0 100644
--- a/back/src/bsdasris/resolvers/mutations/__tests__/createBsdasriEcoOrganisme.integration.ts
+++ b/back/src/bsdasris/resolvers/mutations/__tests__/createBsdasriEcoOrganisme.integration.ts
@@ -129,7 +129,9 @@ describe("Mutation.createDasri", () => {
]);
});
it("create a dasri with an eco-organisme (eco-org user)", async () => {
- const ecoOrg = await ecoOrganismeFactory({ handleBsdasri: true });
+ const ecoOrg = await ecoOrganismeFactory({
+ handle: { handleBsdasri: true }
+ });
const { user } = await userWithCompanyFactory("MEMBER", {
siret: ecoOrg.siret
});
@@ -178,7 +180,9 @@ describe("Mutation.createDasri", () => {
expect(data.createBsdasri.ecoOrganisme?.siret).toEqual(ecoOrg.siret);
});
it("create a dasri with an eco-organisme and an unregistered emitter(eco-org user)", async () => {
- const ecoOrg = await ecoOrganismeFactory({ handleBsdasri: true });
+ const ecoOrg = await ecoOrganismeFactory({
+ handle: { handleBsdasri: true }
+ });
const { user } = await userWithCompanyFactory("MEMBER", {
siret: ecoOrg.siret
});
@@ -229,7 +233,9 @@ describe("Mutation.createDasri", () => {
expect(data.createBsdasri.emitter?.company?.siret).toEqual(siret);
});
it("create a dasri with an eco-organism (emitter user)", async () => {
- const ecoOrg = await ecoOrganismeFactory({ handleBsdasri: true });
+ const ecoOrg = await ecoOrganismeFactory({
+ handle: { handleBsdasri: true }
+ });
const { company: ecoOrgCompany } = await userWithCompanyFactory("MEMBER", {
siret: ecoOrg.siret
});
diff --git a/back/src/bsdasris/resolvers/mutations/__tests__/updateBsdasri.integration.ts b/back/src/bsdasris/resolvers/mutations/__tests__/updateBsdasri.integration.ts
index 2d9cb7ca88..f5855a8e93 100644
--- a/back/src/bsdasris/resolvers/mutations/__tests__/updateBsdasri.integration.ts
+++ b/back/src/bsdasris/resolvers/mutations/__tests__/updateBsdasri.integration.ts
@@ -323,7 +323,9 @@ describe("Mutation.updateBsdasri", () => {
it("should allow eco organisme fields update for INITIAL bsdasris", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
- const ecoOrg = await ecoOrganismeFactory({ handleBsdasri: true });
+ const ecoOrg = await ecoOrganismeFactory({
+ handle: { handleBsdasri: true }
+ });
const { company: ecoOrgCompany } = await userWithCompanyFactory("MEMBER", {
siret: ecoOrg.siret
});
@@ -355,7 +357,9 @@ describe("Mutation.updateBsdasri", () => {
it("should allow eco organisme fields nulling for INITIAL bsdasris", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
- const ecoOrg = await ecoOrganismeFactory({ handleBsdasri: true });
+ const ecoOrg = await ecoOrganismeFactory({
+ handle: { handleBsdasri: true }
+ });
const { company: ecoOrgCompany } = await userWithCompanyFactory("MEMBER", {
siret: ecoOrg.siret
});
@@ -430,7 +434,9 @@ describe("Mutation.updateBsdasri", () => {
});
it("should disallow eco organisme fields update after emission signature", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
- const ecoOrg = await ecoOrganismeFactory({ handleBsdasri: true });
+ const ecoOrg = await ecoOrganismeFactory({
+ handle: { handleBsdasri: true }
+ });
const destination = await userWithCompanyFactory("MEMBER");
await userWithCompanyFactory("MEMBER", {
diff --git a/back/src/bsdasris/sirenify.ts b/back/src/bsdasris/sirenify.ts
index 24abc56877..37478152cd 100644
--- a/back/src/bsdasris/sirenify.ts
+++ b/back/src/bsdasris/sirenify.ts
@@ -1,5 +1,8 @@
import { Prisma } from "@prisma/client";
-import buildSirenify, { nextBuildSirenify } from "../companies/sirenify";
+import buildSirenify, {
+ nextBuildSirenify,
+ NextCompanyInputAccessor
+} from "../companies/sirenify";
import { BsdasriInput, CompanyInput } from "../generated/graphql/types";
const accessors = (input: BsdasriInput) => [
@@ -31,11 +34,11 @@ export const sirenify = buildSirenify(accessors);
const bsdasriCreateInputAccessors = (
input: Prisma.BsdasriCreateInput,
sealedFields: string[] = [] // Tranformations should not be run on sealed fields
-) => [
+): NextCompanyInputAccessor[] => [
{
siret: input?.emitterCompanySiret,
skip: sealedFields.includes("emitterCompanySiret"),
- setter: (input: Prisma.BsdasriCreateInput, companyInput: CompanyInput) => {
+ setter: (input, companyInput) => {
input.emitterCompanyName = companyInput.name;
input.emitterCompanyAddress = companyInput.address;
}
@@ -43,7 +46,7 @@ const bsdasriCreateInputAccessors = (
{
siret: input?.transporterCompanySiret,
skip: sealedFields.includes("transporterCompanySiret"),
- setter: (input: Prisma.BsdasriCreateInput, companyInput: CompanyInput) => {
+ setter: (input, companyInput) => {
input.transporterCompanyName = companyInput.name;
input.transporterCompanyAddress = companyInput.address;
}
@@ -51,10 +54,19 @@ const bsdasriCreateInputAccessors = (
{
siret: input?.destinationCompanySiret,
skip: sealedFields.includes("destinationCompanySiret"),
- setter: (input: Prisma.BsdasriCreateInput, companyInput: CompanyInput) => {
+ setter: (input, companyInput) => {
input.destinationCompanyName = companyInput.name;
input.destinationCompanyAddress = companyInput.address;
}
+ },
+ {
+ siret: input?.ecoOrganismeSiret,
+ skip: sealedFields.includes("ecoOrganismeSiret"),
+ setter: (input, companyInput) => {
+ if (companyInput.name) {
+ input.ecoOrganismeName = companyInput.name;
+ }
+ }
}
];
diff --git a/back/src/bsds/indexation/__tests__/bulkIndexBsds.integration.ts b/back/src/bsds/indexation/__tests__/bulkIndexBsds.integration.ts
index 0be92e230b..8eefe39fb3 100644
--- a/back/src/bsds/indexation/__tests__/bulkIndexBsds.integration.ts
+++ b/back/src/bsds/indexation/__tests__/bulkIndexBsds.integration.ts
@@ -255,10 +255,11 @@ describe("indexAllBsdTypeSync", () => {
});
it("should index BSVHUs synchronously", async () => {
+ const user = await userFactory();
const bsvhus = await Promise.all([
- bsvhuFactory({}),
- bsvhuFactory({}),
- bsvhuFactory({})
+ bsvhuFactory({ userId: user.id }),
+ bsvhuFactory({ userId: user.id }),
+ bsvhuFactory({ userId: user.id })
]);
await indexAllBsdTypeSync({
bsdName: "bsvhu",
@@ -407,10 +408,11 @@ describe("indexAllBsdTypeConcurrently", () => {
});
it("should index BSVHUs using the index queue", async () => {
+ const user = await userFactory();
const bsvhus = await Promise.all([
- bsvhuFactory({}),
- bsvhuFactory({}),
- bsvhuFactory({})
+ bsvhuFactory({ userId: user.id }),
+ bsvhuFactory({ userId: user.id }),
+ bsvhuFactory({ userId: user.id })
]);
const jobs = await indexAllBsdTypeConcurrentJobs({
bsdName: "bsvhu",
diff --git a/back/src/bsds/indexation/bulkIndexBsds.ts b/back/src/bsds/indexation/bulkIndexBsds.ts
index 5b91dbe5a9..08412811f9 100644
--- a/back/src/bsds/indexation/bulkIndexBsds.ts
+++ b/back/src/bsds/indexation/bulkIndexBsds.ts
@@ -29,7 +29,10 @@ import {
BsvhuForElasticInclude,
toBsdElastic as bsvhuToBsdElastic
} from "../../bsvhu/elastic";
-import { toBsdElastic as bspaohToBsdElastic } from "../../bspaoh/elastic";
+import {
+ BspaohForElasticInclude,
+ toBsdElastic as bspaohToBsdElastic
+} from "../../bspaoh/elastic";
import { bulkIndexQueue } from "../../queue/producers/elastic";
import { IndexAllFnSignature, FindManyAndIndexBsdsFnSignature } from "./types";
@@ -78,6 +81,9 @@ const prismaFindManyOptions = {
},
bsdd: {
include: FormForElasticInclude
+ },
+ bspaoh: {
+ include: BspaohForElasticInclude
}
};
diff --git a/back/src/bsds/indexation/reindexBsdHelpers.ts b/back/src/bsds/indexation/reindexBsdHelpers.ts
index 259704f229..fe15007ace 100644
--- a/back/src/bsds/indexation/reindexBsdHelpers.ts
+++ b/back/src/bsds/indexation/reindexBsdHelpers.ts
@@ -3,7 +3,7 @@ import { BsdaForElasticInclude, indexBsda } from "../../bsda/elastic";
import { BsdasriForElasticInclude, indexBsdasri } from "../../bsdasris/elastic";
import { getBsffForElastic, indexBsff } from "../../bsffs/elastic";
import { getBsvhuForElastic, indexBsvhu } from "../../bsvhu/elastic";
-import { indexBspaoh } from "../../bspaoh/elastic";
+import { getBspaohForElastic, indexBspaoh } from "../../bspaoh/elastic";
import { getFormForElastic, indexForm } from "../../forms/elastic";
import { deleteBsd } from "../../common/elastic";
import { getReadonlyBsdaRepository } from "../../bsda/repository";
@@ -75,7 +75,7 @@ export async function reindex(bsdId, exitFn) {
});
if (!!bspaoh && !bspaoh.isDeleted) {
- await indexBspaoh(bspaoh);
+ await indexBspaoh(await getBspaohForElastic(bspaoh));
} else {
await deleteBsd({ id: bsdId });
}
diff --git a/back/src/bsds/resolvers/Mutation.ts b/back/src/bsds/resolvers/Mutation.ts
index 453f21a0fa..7572a813b4 100644
--- a/back/src/bsds/resolvers/Mutation.ts
+++ b/back/src/bsds/resolvers/Mutation.ts
@@ -1,7 +1,12 @@
import { MutationResolvers } from "../../generated/graphql/types";
+import cloneBsdResolver from "./mutations/clone";
import createPdfAccessTokenResolver from "./mutations/createPdfAccessToken";
import reindexBsds from "./mutations/reindexBsds";
+
export const Mutation: MutationResolvers = {
createPdfAccessToken: createPdfAccessTokenResolver,
- reindexBsds: reindexBsds
+ reindexBsds: reindexBsds,
+ ...(process.env.ALLOW_CLONING_BSDS === "true"
+ ? { cloneBsd: cloneBsdResolver }
+ : {})
};
diff --git a/back/src/bsds/resolvers/index.ts b/back/src/bsds/resolvers/index.ts
index 49dc97bda7..bdd3f998fc 100644
--- a/back/src/bsds/resolvers/index.ts
+++ b/back/src/bsds/resolvers/index.ts
@@ -2,10 +2,12 @@ import { QueryResolvers } from "../../generated/graphql/types";
import bsds from "./queries/bsds";
import { bsdResolver } from "./queries/bsd";
import { Mutation } from "./Mutation";
+import controlBsdsResolver from "./queries/controlBsds";
const Query: QueryResolvers = {
bsds,
- bsd: bsdResolver
+ bsd: bsdResolver,
+ controlBsds: controlBsdsResolver
};
export default { Query, Mutation };
diff --git a/back/src/bsds/resolvers/mutations/__tests__/cloneBsd.integration.ts b/back/src/bsds/resolvers/mutations/__tests__/cloneBsd.integration.ts
new file mode 100644
index 0000000000..35f27b1a3e
--- /dev/null
+++ b/back/src/bsds/resolvers/mutations/__tests__/cloneBsd.integration.ts
@@ -0,0 +1,450 @@
+import gql from "graphql-tag";
+import {
+ bsddFinalOperationFactory,
+ formFactory,
+ userFactory
+} from "../../../../__tests__/factories";
+import { Mutation } from "../../../../generated/graphql/types";
+import makeClient from "../../../../__tests__/testClient";
+import { prisma } from "@td/prisma";
+import {
+ bsdaInclude,
+ bsdasriInclude,
+ bsddInclude,
+ bsffInclude,
+ bspaohInclude,
+ bsvhuInclude
+} from "../utils/clone.utils";
+import { isDefinedStrict } from "../../../../common/helpers";
+import {
+ bsdaFactory,
+ bsdaFinalOperationFactory
+} from "../../../../bsda/__tests__/factories";
+import {
+ bsdasriFactory,
+ bsdasriFinalOperationFactory
+} from "../../../../bsdasris/__tests__/factories";
+import {
+ createBsff,
+ createBsffPackagingFinalOperation
+} from "../../../../bsffs/__tests__/factories";
+import { bsvhuFactory } from "../../../../bsvhu/__tests__/factories.vhu";
+import { bspaohFactory } from "../../../../bspaoh/__tests__/factories";
+
+const CLONE_BSD = gql`
+ mutation cloneBsd($id: String!) {
+ cloneBsd(id: $id) {
+ id
+ }
+ }
+`;
+
+const NON_CLONED_KEYS = [
+ "id",
+ "readableId",
+ "rowNumber",
+ "createdAt",
+ "updatedAt",
+ "formId",
+ "bsdaId",
+ "bsffId",
+ "bspaohId",
+ "finalBsdaId",
+ "initialBsdaId",
+ "finalBsdasriId",
+ "initialBsdasriId",
+ "initialFormId",
+ "finalFormId",
+ "finalBsffPackagingId",
+ "initialBsffPackagingId"
+];
+/**
+ * Removes null, undefined and empty strings / arrays from object, AND keys that we
+ * do NOT clone on purpose
+ */
+const removeIrrelevantKeys = obj => {
+ for (const key in obj) {
+ if (
+ !isDefinedStrict(obj[key]) ||
+ NON_CLONED_KEYS.includes(key) ||
+ (Array.isArray(obj[key]) && !obj[key].length)
+ ) {
+ delete obj[key];
+ } else if (typeof obj[key] === "object") {
+ removeIrrelevantKeys(obj[key]);
+ }
+ }
+ return obj;
+};
+
+const clone = obj => JSON.parse(JSON.stringify(obj));
+
+const expectBsdsToMatch = (bsd1, bsd2) => {
+ const cleanedUpBsd1 = removeIrrelevantKeys(clone(bsd1));
+ const cleanedUpBsd2 = removeIrrelevantKeys(clone(bsd2));
+
+ expect(cleanedUpBsd1).toEqual(cleanedUpBsd2);
+};
+
+describe("mutation cloneBsd", () => {
+ it("should clone regular BSDD", async () => {
+ // Given
+ const user = await userFactory();
+ const bsdd = await formFactory({ ownerId: user.id });
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bsdd.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBsdd = await prisma.form.findFirstOrThrow({
+ where: { id: bsdd.id },
+ include: bsddInclude
+ });
+
+ const newBsdd = await prisma.form.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bsddInclude
+ });
+
+ expectBsdsToMatch(initialBsdd, newBsdd);
+ });
+
+ it("should clone BSDD with final operations", async () => {
+ // Given
+ const user = await userFactory();
+ const bsdd = await formFactory({ ownerId: user.id });
+ await bsddFinalOperationFactory({
+ bsddId: bsdd.id,
+ opts: {
+ noTraceability: true,
+ operationCode: "OP CODE",
+ quantity: 101
+ }
+ });
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bsdd.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBsdd = await prisma.form.findFirstOrThrow({
+ where: { id: bsdd.id },
+ include: bsddInclude
+ });
+
+ const newBsdd = await prisma.form.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bsddInclude
+ });
+
+ expectBsdsToMatch(initialBsdd, newBsdd);
+ });
+
+ it("should clone regular BSDA", async () => {
+ // Given
+ const user = await userFactory();
+ const bsda = await bsdaFactory({ userId: user.id });
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bsda.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBsda = await prisma.bsda.findFirstOrThrow({
+ where: { id: bsda.id },
+ include: bsdaInclude
+ });
+
+ const newBsda = await prisma.bsda.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bsdaInclude
+ });
+
+ expectBsdsToMatch(initialBsda, newBsda);
+ });
+
+ it("should clone BSDA with final operations", async () => {
+ // Given
+ const user = await userFactory();
+ const bsda = await bsdaFactory({ userId: user.id });
+ await bsdaFinalOperationFactory({
+ bsdaId: bsda.id,
+ opts: {
+ operationCode: "OP CODE",
+ quantity: 77
+ }
+ });
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bsda.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBsda = await prisma.bsda.findFirstOrThrow({
+ where: { id: bsda.id },
+ include: bsdaInclude
+ });
+
+ const newBsda = await prisma.bsda.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bsdaInclude
+ });
+
+ expectBsdsToMatch(initialBsda, newBsda);
+ });
+
+ it("should clone regular BSDASRI", async () => {
+ // Given
+ const user = await userFactory();
+ const bsdasri = await bsdasriFactory({});
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bsdasri.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBsdasri = await prisma.bsdasri.findFirstOrThrow({
+ where: { id: bsdasri.id },
+ include: bsdasriInclude
+ });
+
+ const newBsdasri = await prisma.bsdasri.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bsdasriInclude
+ });
+
+ expectBsdsToMatch(initialBsdasri, newBsdasri);
+ });
+
+ it("should clone BSDASRI with final operations", async () => {
+ // Given
+ const user = await userFactory();
+ const bsdasri = await bsdasriFactory({});
+ await bsdasriFinalOperationFactory({
+ bsdasriId: bsdasri.id,
+ opts: {
+ operationCode: "OP CODE",
+ quantity: 88
+ }
+ });
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bsdasri.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBsdasri = await prisma.bsdasri.findFirstOrThrow({
+ where: { id: bsdasri.id },
+ include: bsdasriInclude
+ });
+
+ const newBsdasri = await prisma.bsdasri.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bsdasriInclude
+ });
+
+ expectBsdsToMatch(initialBsdasri, newBsdasri);
+ });
+
+ it("should clone regular BSFF", async () => {
+ // Given
+ const user = await userFactory();
+ const bsff = await createBsff();
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bsff.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBsff = await prisma.bsff.findFirstOrThrow({
+ where: { id: bsff.id },
+ include: bsffInclude
+ });
+
+ const newBsff = await prisma.bsff.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bsffInclude
+ });
+
+ expectBsdsToMatch(initialBsff, newBsff);
+ });
+
+ it("should clone BSFF with final operations in packagings", async () => {
+ // Given
+ const user = await userFactory();
+ const bsff = await createBsff();
+ await createBsffPackagingFinalOperation({
+ bsffPackagingId: bsff.packagings[0].id,
+ opts: {
+ noTraceability: true,
+ quantity: 17
+ }
+ });
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bsff.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBsff = await prisma.bsff.findFirstOrThrow({
+ where: { id: bsff.id },
+ include: bsffInclude
+ });
+
+ const newBsff = await prisma.bsff.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bsffInclude
+ });
+
+ expectBsdsToMatch(initialBsff, newBsff);
+
+ const initialPackagings = await prisma.bsffPackaging.findMany({
+ where: { bsffId: initialBsff.id },
+ include: { finalOperations: true }
+ });
+
+ const newPackagings = await prisma.bsffPackaging.findMany({
+ where: { bsffId: newBsff.id },
+ include: { finalOperations: true }
+ });
+
+ expectBsdsToMatch(initialPackagings, newPackagings);
+ });
+
+ it("should clone regular BSVHU", async () => {
+ // Given
+ const user = await userFactory();
+ const bsvhu = await bsvhuFactory({});
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bsvhu.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBsvhu = await prisma.bsvhu.findFirstOrThrow({
+ where: { id: bsvhu.id },
+ include: bsvhuInclude
+ });
+
+ const newBsvhu = await prisma.bsvhu.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bsvhuInclude
+ });
+
+ expectBsdsToMatch(initialBsvhu, newBsvhu);
+ });
+
+ it("should clone regular BSPAOH", async () => {
+ // Given
+ const user = await userFactory();
+ const bspaoh = await bspaohFactory({});
+
+ // When
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate>(
+ CLONE_BSD,
+ {
+ variables: {
+ id: bspaoh.id
+ }
+ }
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ const initialBspaoh = await prisma.bspaoh.findFirstOrThrow({
+ where: { id: bspaoh.id },
+ include: bspaohInclude
+ });
+
+ const newBspaoh = await prisma.bspaoh.findFirstOrThrow({
+ where: { id: data.cloneBsd.id },
+ include: bspaohInclude
+ });
+
+ expectBsdsToMatch(initialBspaoh, newBspaoh);
+ });
+});
diff --git a/back/src/bsds/resolvers/mutations/__tests__/createPdfAccessToken.integration.ts b/back/src/bsds/resolvers/mutations/__tests__/createPdfAccessToken.integration.ts
index 7cec571b65..24cf7bff4b 100644
--- a/back/src/bsds/resolvers/mutations/__tests__/createPdfAccessToken.integration.ts
+++ b/back/src/bsds/resolvers/mutations/__tests__/createPdfAccessToken.integration.ts
@@ -92,7 +92,7 @@ describe("Mutation.creatPdfAccessToken", () => {
})
]);
});
- it("should deny bsdd token creation for non SENT status", async () => {
+ it("should deny bsdd token creation for non SENT status and no citerne/ADR business", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const bsdd = await formFactory({
ownerId: user.id,
@@ -117,7 +117,7 @@ describe("Mutation.creatPdfAccessToken", () => {
expect(errors).toEqual([
expect.objectContaining({
message:
- "Seuls les bordereaux pris en charge par un transporteur peuvent être consulté via un accès temporaire.",
+ "Seuls les bordereaux pris en charge par un transporteur peuvent être consultés via un accès temporaire.",
extensions: expect.objectContaining({
code: ErrorCode.FORBIDDEN
})
@@ -157,6 +157,45 @@ describe("Mutation.creatPdfAccessToken", () => {
// token lifespan should be 30'
expect(token.expiresAt.getTime() > addMinutes(new Date(), 29).getTime());
});
+
+ it("should create a token for a bsdd if status is not OK but belongs to isReturnTab", async () => {
+ // Given
+ const { user, company } = await userWithCompanyFactory("MEMBER");
+ const bsdd = await formFactory({
+ ownerId: user.id,
+ opt: {
+ transporters: {
+ create: { transporterCompanySiret: company.siret, number: 1 }
+ },
+ status: Status.RECEIVED,
+ receivedAt: new Date(),
+ emptyReturnADR: "EMPTY_CITERNE"
+ }
+ });
+
+ // When
+ const { mutate } = makeClient(user);
+ const { data } = await mutate>(
+ CREATE_PDF_TOKEN,
+ {
+ variables: {
+ input: { bsdId: bsdd.id }
+ }
+ }
+ );
+
+ // Then
+ const token = await prisma.pdfAccessToken.findFirstOrThrow({
+ where: { bsdType: BsdType.BSDD, userId: user.id, bsdId: bsdd.id }
+ });
+
+ expect(data.createPdfAccessToken).toEqual(
+ `http://api.trackdechets.local/road-control/${token.token}`
+ );
+ // token lifespan should be 30'
+ expect(token.expiresAt.getTime() > addMinutes(new Date(), 29).getTime());
+ });
+
it("should deny token creation for a bsda if user does not belong to it", async () => {
const { company } = await userWithCompanyFactory("MEMBER");
const user = await userFactory();
@@ -211,7 +250,7 @@ describe("Mutation.creatPdfAccessToken", () => {
expect(errors).toEqual([
expect.objectContaining({
message:
- "Seuls les bordereaux pris en charge par un transporteur peuvent être consulté via un accès temporaire.",
+ "Seuls les bordereaux pris en charge par un transporteur peuvent être consultés via un accès temporaire.",
extensions: expect.objectContaining({
code: ErrorCode.FORBIDDEN
})
@@ -298,7 +337,7 @@ describe("Mutation.creatPdfAccessToken", () => {
expect(errors).toEqual([
expect.objectContaining({
message:
- "Seuls les bordereaux pris en charge par un transporteur peuvent être consulté via un accès temporaire.",
+ "Seuls les bordereaux pris en charge par un transporteur peuvent être consultés via un accès temporaire.",
extensions: expect.objectContaining({
code: ErrorCode.FORBIDDEN
})
@@ -380,7 +419,7 @@ describe("Mutation.creatPdfAccessToken", () => {
expect(errors).toEqual([
expect.objectContaining({
message:
- "Seuls les bordereaux pris en charge par un transporteur peuvent être consulté via un accès temporaire.",
+ "Seuls les bordereaux pris en charge par un transporteur peuvent être consultés via un accès temporaire.",
extensions: expect.objectContaining({
code: ErrorCode.FORBIDDEN
})
@@ -464,7 +503,7 @@ describe("Mutation.creatPdfAccessToken", () => {
expect(errors).toEqual([
expect.objectContaining({
message:
- "Seuls les bordereaux pris en charge par un transporteur peuvent être consulté via un accès temporaire.",
+ "Seuls les bordereaux pris en charge par un transporteur peuvent être consultés via un accès temporaire.",
extensions: expect.objectContaining({
code: ErrorCode.FORBIDDEN
})
diff --git a/back/src/bsds/resolvers/mutations/clone.ts b/back/src/bsds/resolvers/mutations/clone.ts
new file mode 100644
index 0000000000..74454b82fd
--- /dev/null
+++ b/back/src/bsds/resolvers/mutations/clone.ts
@@ -0,0 +1,44 @@
+import { MutationResolvers } from "../../../generated/graphql/types";
+import { applyAuthStrategies, AuthType } from "../../../auth";
+import { z } from "zod";
+import {
+ cloneBsda,
+ cloneBsdasri,
+ cloneBsff,
+ cloneBsvhu,
+ cloneBspaoh,
+ cloneBsdd
+} from "./utils/clone.utils";
+import { checkIsAuthenticated } from "../../../common/permissions";
+
+const idSchema = z.string().min(15).max(50);
+
+const cloneBsdResolver: MutationResolvers["cloneBsd"] = async (
+ _,
+ { id },
+ context
+) => {
+ applyAuthStrategies(context, [AuthType.Session]);
+ const user = checkIsAuthenticated(context);
+
+ await idSchema.parse(id);
+
+ let newBsd;
+ if (id.startsWith("BSDA-")) {
+ newBsd = await cloneBsda(user, id);
+ } else if (id.startsWith("DASRI-")) {
+ newBsd = await cloneBsdasri(user, id);
+ } else if (id.startsWith("FF-")) {
+ newBsd = await cloneBsff(user, id);
+ } else if (id.startsWith("VHU-")) {
+ newBsd = await cloneBsvhu(user, id);
+ } else if (id.startsWith("PAOH-")) {
+ newBsd = await cloneBspaoh(user, id);
+ } else {
+ newBsd = await cloneBsdd(user, id);
+ }
+
+ return { id: newBsd.id };
+};
+
+export default cloneBsdResolver;
diff --git a/back/src/bsds/resolvers/mutations/createPdfAccessToken.ts b/back/src/bsds/resolvers/mutations/createPdfAccessToken.ts
index 868a58b4ce..5f70c764ac 100644
--- a/back/src/bsds/resolvers/mutations/createPdfAccessToken.ts
+++ b/back/src/bsds/resolvers/mutations/createPdfAccessToken.ts
@@ -27,6 +27,12 @@ import { ROAD_CONTROL_SLUG } from "@td/constants";
import { checkCanRead as checkCanReadForm } from "../../../forms/permissions";
import { checkCanRead as checkCanReadBsdasri } from "../../../bsdasris/permissions";
import { ForbiddenError } from "../../../common/errors";
+import { belongsToIsReturnForTab as formBelongsToIsReturnForTab } from "../../../forms/elasticHelpers";
+import { belongsToIsReturnForTab as bsdaBelongsToIsReturnForTab } from "../../../bsda/elastic";
+import { belongsToIsReturnForTab as bsdasriBelongsToIsReturnForTab } from "../../../bsdasris/elastic";
+import { belongsToIsReturnForTab as bsffBelongsToIsReturnForTab } from "../../../bsffs/elastic";
+import { belongsToIsReturnForTab as bsvhuBelongsToIsReturnForTab } from "../../../bsvhu/elastic";
+import { belongsToIsReturnForTab as bspaohBelongsToIsReturnForTab } from "../../../bspaoh/elastic";
const accessors = {
[BsdType.BSDD]: id =>
@@ -56,6 +62,15 @@ const checkStatus = {
[BsdType.BSPAOH]: bsd => bsd.status === BspaohStatus.SENT
};
+const checkBelongsToIsReturnForTab = {
+ [BsdType.BSDD]: bsd => formBelongsToIsReturnForTab(bsd),
+ [BsdType.BSDA]: bsd => bsdaBelongsToIsReturnForTab(bsd),
+ [BsdType.BSDASRI]: bsd => bsdasriBelongsToIsReturnForTab(bsd),
+ [BsdType.BSFF]: bsd => bsffBelongsToIsReturnForTab(bsd),
+ [BsdType.BSVHU]: bsd => bsvhuBelongsToIsReturnForTab(bsd),
+ [BsdType.BSPAOH]: bsd => bspaohBelongsToIsReturnForTab(bsd)
+};
+
const permissions = {
[BsdType.BSDD]: (user, bsdd) => checkCanReadForm(user, bsdd),
[BsdType.BSDA]: (user, bsda) => checkCanReadBsda(user, bsda),
@@ -97,10 +112,13 @@ const createPdfAccessToken: MutationResolvers["createPdfAccessToken"] = async (
const bsd = await accessors[bsdType](input.bsdId);
- // check status
- if (!checkStatus[bsdType](bsd)) {
+ // Check status
+ if (
+ !checkStatus[bsdType](bsd) &&
+ !checkBelongsToIsReturnForTab[bsdType](bsd)
+ ) {
throw new ForbiddenError(
- "Seuls les bordereaux pris en charge par un transporteur peuvent être consulté via un accès temporaire."
+ "Seuls les bordereaux pris en charge par un transporteur peuvent être consultés via un accès temporaire."
);
}
// check perms
diff --git a/back/src/bsds/resolvers/mutations/utils/clone.utils.ts b/back/src/bsds/resolvers/mutations/utils/clone.utils.ts
new file mode 100644
index 0000000000..489c1919f9
--- /dev/null
+++ b/back/src/bsds/resolvers/mutations/utils/clone.utils.ts
@@ -0,0 +1,967 @@
+import { Form, Prisma } from "@prisma/client";
+import { prisma } from "@td/prisma";
+import getReadableId, { ReadableIdPrefix } from "../../../../forms/readableId";
+import { getFormRepository } from "../../../../forms/repository";
+import { prismaJsonNoNull } from "../../../../common/converter";
+import { FormWithTransporters } from "../../../../forms/types";
+import { UserInputError } from "../../../../common/errors";
+import { getBsdaRepository } from "../../../../bsda/repository";
+import { getBsdasriRepository } from "../../../../bsdasris/repository";
+import { getBsffRepository } from "../../../../bsffs/repository";
+import { getBsvhuRepository } from "../../../../bsvhu/repository";
+import { getBspaohRepository } from "../../../../bspaoh/repository";
+
+export const bsdaInclude = {
+ transporters: true,
+ intermediaries: true,
+ finalOperations: true,
+ grouping: true,
+ groupedIn: true,
+ forwarding: true,
+ forwardedIn: true
+};
+
+export const cloneBsda = async (user: Express.User, id: string) => {
+ const bsda = await prisma.bsda.findFirstOrThrow({
+ where: { id },
+ include: bsdaInclude
+ });
+
+ if (!bsda) {
+ throw new UserInputError(`ID invalide ${id}`);
+ }
+
+ if (
+ bsda.grouping?.length ||
+ bsda.groupedIn ||
+ bsda.forwarding ||
+ bsda.forwardedIn
+ ) {
+ throw new UserInputError(
+ "Impossible de cloner ce type de BSD pour le moment"
+ );
+ }
+
+ const { create } = getBsdaRepository(user);
+
+ const newBsdaCreateInput: Omit<
+ Required,
+ // Ignored for the time being
+ | "rowNumber"
+ | "bsdaRevisionRequests"
+ | "FinalOperationToFinalForm"
+ | "forwardedIn"
+ | "forwarding"
+ | "groupedIn"
+ | "grouping"
+ | "rowNumber"
+ // Done manually
+ | "finalOperations"
+ > = {
+ id: getReadableId(ReadableIdPrefix.BSDA),
+ brokerCompanyAddress: bsda.brokerCompanyAddress,
+ brokerCompanyContact: bsda.brokerCompanyContact,
+ brokerCompanyMail: bsda.brokerCompanyMail,
+ brokerCompanyName: bsda.brokerCompanyName,
+ brokerCompanyPhone: bsda.brokerCompanyPhone,
+ brokerCompanySiret: bsda.brokerCompanySiret,
+ brokerRecepisseDepartment: bsda.brokerRecepisseDepartment,
+ brokerRecepisseNumber: bsda.brokerRecepisseNumber,
+ brokerRecepisseValidityLimit: bsda.brokerRecepisseValidityLimit,
+ canAccessDraftOrgIds: bsda.canAccessDraftOrgIds,
+ createdAt: bsda.createdAt,
+ destinationCap: bsda.destinationCap,
+ destinationCompanyAddress: bsda.destinationCompanyAddress,
+ destinationCompanyContact: bsda.destinationCompanyContact,
+ destinationCompanyMail: bsda.destinationCompanyMail,
+ destinationCompanyName: bsda.destinationCompanyName,
+ destinationCompanyPhone: bsda.destinationCompanyPhone,
+ destinationCompanySiret: bsda.destinationCompanySiret,
+ destinationCustomInfo: bsda.destinationCustomInfo,
+ destinationOperationCode: bsda.destinationOperationCode,
+ destinationOperationDate: bsda.destinationOperationDate,
+ destinationOperationDescription: bsda.destinationOperationDescription,
+ destinationOperationMode: bsda.destinationOperationMode,
+ destinationOperationNextDestinationCap:
+ bsda.destinationOperationNextDestinationCap,
+ destinationOperationNextDestinationCompanyAddress:
+ bsda.destinationOperationNextDestinationCompanyAddress,
+ destinationOperationNextDestinationCompanyContact:
+ bsda.destinationOperationNextDestinationCompanyContact,
+ destinationOperationNextDestinationCompanyMail:
+ bsda.destinationOperationNextDestinationCompanyMail,
+ destinationOperationNextDestinationCompanyName:
+ bsda.destinationOperationNextDestinationCompanyName,
+ destinationOperationNextDestinationCompanyPhone:
+ bsda.destinationOperationNextDestinationCompanyPhone,
+ destinationOperationNextDestinationCompanySiret:
+ bsda.destinationOperationNextDestinationCompanySiret,
+ destinationOperationNextDestinationCompanyVatNumber:
+ bsda.destinationOperationNextDestinationCompanyVatNumber,
+ destinationOperationNextDestinationPlannedOperationCode:
+ bsda.destinationOperationNextDestinationPlannedOperationCode,
+ destinationOperationSignatureAuthor:
+ bsda.destinationOperationSignatureAuthor,
+ destinationOperationSignatureDate: bsda.destinationOperationSignatureDate,
+ destinationPlannedOperationCode: bsda.destinationPlannedOperationCode,
+ destinationReceptionAcceptationStatus:
+ bsda.destinationReceptionAcceptationStatus,
+ destinationReceptionDate: bsda.destinationReceptionDate,
+ destinationReceptionRefusalReason: bsda.destinationReceptionRefusalReason,
+ destinationReceptionWeight: bsda.destinationReceptionWeight,
+ ecoOrganismeName: bsda.ecoOrganismeName,
+ ecoOrganismeSiret: bsda.ecoOrganismeSiret,
+ emitterCompanyAddress: bsda.emitterCompanyAddress,
+ emitterCompanyContact: bsda.emitterCompanyContact,
+ emitterCompanyMail: bsda.emitterCompanyMail,
+ emitterCompanyName: bsda.emitterCompanyName,
+ emitterCompanyPhone: bsda.emitterCompanyPhone,
+ emitterCompanySiret: bsda.emitterCompanySiret,
+ emitterCustomInfo: bsda.emitterCustomInfo,
+ emitterEmissionSignatureAuthor: bsda.emitterEmissionSignatureAuthor,
+ emitterEmissionSignatureDate: bsda.emitterEmissionSignatureDate,
+ emitterIsPrivateIndividual: bsda.emitterIsPrivateIndividual,
+ emitterPickupSiteAddress: bsda.emitterPickupSiteAddress,
+ emitterPickupSiteCity: bsda.emitterPickupSiteCity,
+ emitterPickupSiteInfos: bsda.emitterPickupSiteInfos,
+ emitterPickupSiteName: bsda.emitterPickupSiteName,
+ emitterPickupSitePostalCode: bsda.emitterPickupSitePostalCode,
+ intermediaries: bsda.intermediaries.length
+ ? {
+ createMany: {
+ data: bsda.intermediaries
+ }
+ }
+ : {},
+ intermediariesOrgIds: bsda.intermediariesOrgIds,
+ isDeleted: bsda.isDeleted,
+ isDraft: bsda.isDraft,
+ packagings: prismaJsonNoNull(bsda.packagings),
+ status: bsda.status,
+ transporters: bsda.transporters.length
+ ? {
+ createMany: {
+ data: bsda.transporters!.map((t, idx) => {
+ const { id, ...data } = t;
+ return { ...data, bsdaId: undefined, number: idx + 1 };
+ })
+ }
+ }
+ : {},
+ transportersOrgIds: bsda.transportersOrgIds,
+ transporterTransportSignatureDate: bsda.transporterTransportSignatureDate,
+ type: bsda.type,
+ updatedAt: bsda.updatedAt,
+ wasteAdr: bsda.wasteAdr,
+ wasteCode: bsda.wasteCode,
+ wasteConsistence: bsda.wasteConsistence,
+ wasteFamilyCode: bsda.wasteFamilyCode,
+ wasteMaterialName: bsda.wasteMaterialName,
+ wastePop: bsda.wastePop,
+ wasteSealNumbers: bsda.wasteSealNumbers,
+ weightIsEstimate: bsda.weightIsEstimate,
+ weightValue: bsda.weightValue,
+ workerCertificationCertificationNumber:
+ bsda.workerCertificationCertificationNumber,
+ workerCertificationHasSubSectionFour:
+ bsda.workerCertificationHasSubSectionFour,
+ workerCertificationHasSubSectionThree:
+ bsda.workerCertificationHasSubSectionThree,
+ workerCertificationOrganisation: bsda.workerCertificationOrganisation,
+ workerCertificationValidityLimit: bsda.workerCertificationValidityLimit,
+ workerCompanyAddress: bsda.workerCompanyAddress,
+ workerCompanyContact: bsda.workerCompanyContact,
+ workerCompanyMail: bsda.workerCompanyMail,
+ workerCompanyName: bsda.workerCompanyName,
+ workerCompanyPhone: bsda.workerCompanyPhone,
+ workerCompanySiret: bsda.workerCompanySiret,
+ workerIsDisabled: bsda.workerIsDisabled,
+ workerWorkHasEmitterPaperSignature: bsda.workerWorkHasEmitterPaperSignature,
+ workerWorkSignatureAuthor: bsda.workerWorkSignatureAuthor,
+ workerWorkSignatureDate: bsda.workerWorkSignatureDate
+ };
+
+ const newBsda = await create(newBsdaCreateInput);
+
+ // Final operations
+ for await (const operation of bsda.finalOperations) {
+ const finalOperation: Prisma.BsdaFinalOperationCreateInput = {
+ initialBsda: { connect: { id: newBsda.id } },
+ finalBsda: { connect: { id: newBsda.id } },
+ operationCode: operation.operationCode,
+ quantity: operation.quantity
+ };
+
+ await prisma.bsdaFinalOperation.create({
+ data: finalOperation
+ });
+ }
+
+ return newBsda;
+};
+
+export const bsdasriInclude = {
+ finalOperations: true,
+ grouping: true,
+ groupedIn: true,
+ synthesizedIn: true,
+ synthesizing: true
+};
+
+export const cloneBsdasri = async (user: Express.User, id: string) => {
+ const bsdasri = await prisma.bsdasri.findFirstOrThrow({
+ where: { id },
+ include: bsdasriInclude
+ });
+
+ if (!bsdasri) {
+ throw new UserInputError(`ID invalide ${id}`);
+ }
+
+ if (
+ bsdasri.grouping?.length ||
+ bsdasri.groupedIn ||
+ bsdasri.synthesizedInId ||
+ bsdasri.synthesizedIn ||
+ bsdasri.synthesizing.length
+ ) {
+ throw new UserInputError(
+ "Impossible de cloner ce type de BSD pour le moment"
+ );
+ }
+
+ const { create } = getBsdasriRepository(user);
+
+ const newBsdasriCreateInput: Omit<
+ Required,
+ // Ignored for the time being
+ | "bsdasriRevisionRequests"
+ | "FinalOperationToFinalBsdasri"
+ | "groupedIn"
+ | "grouping"
+ | "synthesizedIn"
+ | "synthesizing"
+ | "rowNumber"
+ // Done manually
+ | "finalOperations"
+ > = {
+ id: getReadableId(ReadableIdPrefix.DASRI),
+ createdAt: bsdasri.createdAt,
+ destinationCompanyAddress: bsdasri.destinationCompanyAddress,
+ destinationCompanyContact: bsdasri.destinationCompanyContact,
+ destinationCompanyMail: bsdasri.destinationCompanyMail,
+ destinationCompanyName: bsdasri.destinationCompanyName,
+ destinationCompanyPhone: bsdasri.destinationCompanyPhone,
+ destinationCompanySiret: bsdasri.destinationCompanySiret,
+ destinationCustomInfo: bsdasri.destinationCustomInfo,
+ destinationOperationCode: bsdasri.destinationOperationCode,
+ destinationOperationDate: bsdasri.destinationOperationDate,
+ destinationOperationMode: bsdasri.destinationOperationMode,
+ destinationOperationSignatureAuthor:
+ bsdasri.destinationOperationSignatureAuthor,
+ destinationOperationSignatureDate:
+ bsdasri.destinationOperationSignatureDate,
+ destinationReceptionAcceptationStatus:
+ bsdasri.destinationReceptionAcceptationStatus,
+ destinationReceptionDate: bsdasri.destinationReceptionDate,
+ destinationReceptionSignatureAuthor:
+ bsdasri.destinationReceptionSignatureAuthor,
+ destinationReceptionSignatureDate:
+ bsdasri.destinationReceptionSignatureDate,
+ destinationReceptionWasteRefusalReason:
+ bsdasri.destinationReceptionWasteRefusalReason,
+ destinationReceptionWasteRefusedWeightValue:
+ bsdasri.destinationReceptionWasteRefusedWeightValue,
+ destinationReceptionWasteVolume: bsdasri.destinationReceptionWasteVolume,
+ destinationReceptionWasteWeightValue:
+ bsdasri.destinationReceptionWasteWeightValue,
+ destinationWastePackagings: prismaJsonNoNull(
+ bsdasri.destinationWastePackagings
+ ),
+ ecoOrganismeName: bsdasri.ecoOrganismeName,
+ ecoOrganismeSiret: bsdasri.ecoOrganismeSiret,
+ emissionSignatory: bsdasri.emissionSignatoryId
+ ? {
+ connect: {
+ id: bsdasri.emissionSignatoryId
+ }
+ }
+ : {},
+ emittedByEcoOrganisme: bsdasri.emittedByEcoOrganisme,
+ emitterCompanyAddress: bsdasri.emitterCompanyAddress,
+ emitterCompanyContact: bsdasri.emitterCompanyContact,
+ emitterCompanyMail: bsdasri.emitterCompanyMail,
+ emitterCompanyName: bsdasri.emitterCompanyName,
+ emitterCompanyPhone: bsdasri.emitterCompanyPhone,
+ emitterCompanySiret: bsdasri.emitterCompanySiret,
+ emitterCustomInfo: bsdasri.emitterCustomInfo,
+ emitterEmissionSignatureAuthor: bsdasri.emitterEmissionSignatureAuthor,
+ emitterEmissionSignatureDate: bsdasri.emitterEmissionSignatureDate,
+ emitterPickupSiteAddress: bsdasri.emitterPickupSiteAddress,
+ emitterPickupSiteCity: bsdasri.emitterPickupSiteCity,
+ emitterPickupSiteInfos: bsdasri.emitterPickupSiteInfos,
+ emitterPickupSiteName: bsdasri.emitterPickupSiteName,
+ emitterPickupSitePostalCode: bsdasri.emitterPickupSitePostalCode,
+ emitterWastePackagings: prismaJsonNoNull(bsdasri.emitterWastePackagings),
+ emitterWasteVolume: bsdasri.emitterWasteVolume,
+ emitterWasteWeightIsEstimate: bsdasri.emitterWasteWeightIsEstimate,
+ emitterWasteWeightValue: bsdasri.emitterWasteWeightValue,
+ groupingEmitterSirets: bsdasri.groupingEmitterSirets,
+ handedOverToRecipientAt: bsdasri.handedOverToRecipientAt,
+ identificationNumbers: bsdasri.identificationNumbers,
+ isDeleted: bsdasri.isDeleted,
+ isDraft: bsdasri.isDraft,
+ isEmissionDirectTakenOver: bsdasri.isEmissionDirectTakenOver,
+ isEmissionTakenOverWithSecretCode:
+ bsdasri.isEmissionTakenOverWithSecretCode,
+ operationSignatory: bsdasri.operationSignatoryId
+ ? {
+ connect: {
+ id: bsdasri.operationSignatoryId
+ }
+ }
+ : {},
+ receptionSignatory: bsdasri.receptionSignatoryId
+ ? {
+ connect: {
+ id: bsdasri.receptionSignatoryId
+ }
+ }
+ : {},
+ status: bsdasri.status,
+ synthesisEmitterSirets: bsdasri.synthesisEmitterSirets,
+ transporterAcceptationStatus: bsdasri.transporterAcceptationStatus,
+ transporterCompanyAddress: bsdasri.transporterCompanyAddress,
+ transporterCompanyContact: bsdasri.transporterCompanyContact,
+ transporterCompanyMail: bsdasri.transporterCompanyMail,
+ transporterCompanyName: bsdasri.transporterCompanyName,
+ transporterCompanyPhone: bsdasri.transporterCompanyPhone,
+ transporterCompanySiret: bsdasri.transporterCompanySiret,
+ transporterCompanyVatNumber: bsdasri.transporterCompanyVatNumber,
+ transporterCustomInfo: bsdasri.transporterCustomInfo,
+ transporterRecepisseDepartment: bsdasri.transporterRecepisseDepartment,
+ transporterRecepisseIsExempted: bsdasri.transporterRecepisseIsExempted,
+ transporterRecepisseNumber: bsdasri.transporterRecepisseNumber,
+ transporterRecepisseValidityLimit:
+ bsdasri.transporterRecepisseValidityLimit,
+ transporterTakenOverAt: bsdasri.transporterTakenOverAt,
+ transporterTransportMode: bsdasri.transporterTransportMode,
+ transporterTransportPlates: bsdasri.transporterTransportPlates,
+ transporterTransportSignatureAuthor:
+ bsdasri.transporterTransportSignatureAuthor,
+ transporterTransportSignatureDate:
+ bsdasri.transporterTransportSignatureDate,
+ transporterWastePackagings: prismaJsonNoNull(
+ bsdasri.transporterWastePackagings
+ ),
+ transporterWasteRefusalReason: bsdasri.transporterWasteRefusalReason,
+ transporterWasteRefusedWeightValue:
+ bsdasri.transporterWasteRefusedWeightValue,
+ transporterWasteVolume: bsdasri.transporterWasteVolume,
+ transporterWasteWeightIsEstimate: bsdasri.transporterWasteWeightIsEstimate,
+ transporterWasteWeightValue: bsdasri.transporterWasteWeightValue,
+ transportSignatory: bsdasri.transportSignatoryId
+ ? {
+ connect: {
+ id: bsdasri.transportSignatoryId
+ }
+ }
+ : {},
+ type: bsdasri.type,
+ updatedAt: bsdasri.updatedAt,
+ wasteAdr: bsdasri.wasteAdr,
+ wasteCode: bsdasri.wasteCode
+ };
+
+ const newBsdasri = await create(newBsdasriCreateInput);
+
+ // Final operations
+ for await (const operation of bsdasri.finalOperations) {
+ const finalOperation: Prisma.BsdasriFinalOperationCreateInput = {
+ initialBsdasri: { connect: { id: newBsdasri.id } },
+ finalBsdasri: { connect: { id: newBsdasri.id } },
+ operationCode: operation.operationCode,
+ quantity: operation.quantity
+ };
+
+ await prisma.bsdasriFinalOperation.create({
+ data: finalOperation
+ });
+ }
+
+ return newBsdasri;
+};
+
+export const bsffInclude = {
+ transporters: true,
+ ficheInterventions: true,
+ packagings: true
+};
+
+export const cloneBsff = async (user: Express.User, id: string) => {
+ const bsff = await prisma.bsff.findFirstOrThrow({
+ where: { id },
+ include: bsffInclude
+ });
+
+ if (!bsff) {
+ throw new UserInputError(`ID invalide ${id}`);
+ }
+
+ const { create } = getBsffRepository(user);
+
+ const newBsffCreateInput: Omit<
+ Required,
+ // Ignored for the time being
+ "rowNumber"
+ > = {
+ id: getReadableId(ReadableIdPrefix.FF),
+ createdAt: bsff.createdAt,
+ destinationCap: bsff.destinationCap,
+ destinationCompanyAddress: bsff.destinationCompanyAddress,
+ destinationCompanyContact: bsff.destinationCompanyContact,
+ destinationCompanyMail: bsff.destinationCompanyMail,
+ destinationCompanyName: bsff.destinationCompanyName,
+ destinationCompanyPhone: bsff.destinationCompanyPhone,
+ destinationCompanySiret: bsff.destinationCompanySiret,
+ destinationCustomInfo: bsff.destinationCustomInfo,
+ destinationPlannedOperationCode: bsff.destinationPlannedOperationCode,
+ destinationReceptionDate: bsff.destinationReceptionDate,
+ destinationReceptionSignatureAuthor:
+ bsff.destinationReceptionSignatureAuthor,
+ destinationReceptionSignatureDate: bsff.destinationReceptionSignatureDate,
+ detenteurCompanySirets: bsff.detenteurCompanySirets,
+ emitterCompanyAddress: bsff.emitterCompanyAddress,
+ emitterCompanyContact: bsff.emitterCompanyContact,
+ emitterCompanyMail: bsff.emitterCompanyMail,
+ emitterCompanyName: bsff.emitterCompanyName,
+ emitterCompanyPhone: bsff.emitterCompanyPhone,
+ emitterCompanySiret: bsff.emitterCompanySiret,
+ emitterCustomInfo: bsff.emitterCustomInfo,
+ emitterEmissionSignatureAuthor: bsff.emitterEmissionSignatureAuthor,
+ emitterEmissionSignatureDate: bsff.emitterEmissionSignatureDate,
+ ficheInterventions: bsff.ficheInterventions.length
+ ? { create: bsff.ficheInterventions[0] }
+ : {},
+ isDeleted: bsff.isDeleted,
+ isDraft: bsff.isDraft,
+ packagings: bsff.packagings.length
+ ? {
+ createMany: {
+ data: bsff.packagings!.map(t => {
+ const { id, bsffId, ...data } = t;
+ return data;
+ })
+ }
+ }
+ : {},
+ status: bsff.status,
+ transporters: bsff.transporters.length
+ ? {
+ createMany: {
+ data: bsff.transporters!.map((t, idx) => {
+ const { id, bsffId, ...data } = t;
+ return { ...data, number: idx + 1 };
+ })
+ }
+ }
+ : {},
+ transportersOrgIds: bsff.transportersOrgIds,
+ transporterTransportSignatureDate: bsff.transporterTransportSignatureDate,
+ type: bsff.type,
+ updatedAt: bsff.updatedAt,
+ wasteAdr: bsff.wasteAdr,
+ wasteCode: bsff.wasteCode,
+ wasteDescription: bsff.wasteDescription,
+ weightIsEstimate: bsff.weightIsEstimate,
+ weightValue: bsff.weightValue
+ };
+
+ const newBsff = await create({ data: newBsffCreateInput });
+
+ // Final operations in packagings
+ const initialBsffPackagings = await prisma.bsffPackaging.findMany({
+ where: { bsffId: bsff.id },
+ include: { finalOperations: true }
+ });
+ const newBsffPackagings = await prisma.bsffPackaging.findMany({
+ where: { bsffId: newBsff.id }
+ });
+
+ for (let i = 0; i < initialBsffPackagings.length; i++) {
+ if (initialBsffPackagings[i].finalOperations.length) {
+ for await (const finalOperation of initialBsffPackagings[i]
+ .finalOperations) {
+ const finalOperationCreateInput: Prisma.BsffPackagingFinalOperationCreateInput =
+ {
+ initialBsffPackaging: { connect: { id: newBsffPackagings[i].id } },
+ finalBsffPackaging: { connect: { id: newBsffPackagings[i].id } },
+ quantity: finalOperation.quantity,
+ noTraceability: finalOperation.noTraceability,
+ operationCode: finalOperation.operationCode
+ };
+
+ await prisma.bsffPackagingFinalOperation.create({
+ data: finalOperationCreateInput
+ });
+ }
+ }
+ }
+
+ return newBsff;
+};
+
+export const bsvhuInclude = {
+ intermediaries: true
+};
+
+export const cloneBsvhu = async (user: Express.User, id: string) => {
+ const bsvhu = await prisma.bsvhu.findFirstOrThrow({
+ where: { id },
+ include: bsvhuInclude
+ });
+
+ if (!bsvhu) {
+ throw new UserInputError(`ID invalide ${id}`);
+ }
+
+ const { create } = getBsvhuRepository(user);
+
+ const newBsvhuCreateInput: Omit<
+ Required,
+ // Ignored for the time being
+ "rowNumber"
+ > = {
+ id: getReadableId(ReadableIdPrefix.VHU),
+ createdAt: bsvhu.createdAt,
+ destinationAgrementNumber: bsvhu.destinationAgrementNumber,
+ destinationCompanyAddress: bsvhu.destinationCompanyAddress,
+ destinationCompanyContact: bsvhu.destinationCompanyContact,
+ destinationCompanyMail: bsvhu.destinationCompanyMail,
+ destinationCompanyName: bsvhu.destinationCompanyName,
+ destinationCompanyPhone: bsvhu.destinationCompanyPhone,
+ destinationCompanySiret: bsvhu.destinationCompanySiret,
+ destinationCustomInfo: bsvhu.destinationCustomInfo,
+ destinationOperationCode: bsvhu.destinationOperationCode,
+ destinationOperationDate: bsvhu.destinationOperationDate,
+ destinationOperationMode: bsvhu.destinationOperationMode,
+ destinationOperationNextDestinationCompanyAddress:
+ bsvhu.destinationOperationNextDestinationCompanyAddress,
+ destinationOperationNextDestinationCompanyContact:
+ bsvhu.destinationOperationNextDestinationCompanyContact,
+ destinationOperationNextDestinationCompanyMail:
+ bsvhu.destinationOperationNextDestinationCompanyMail,
+ destinationOperationNextDestinationCompanyName:
+ bsvhu.destinationOperationNextDestinationCompanyName,
+ destinationOperationNextDestinationCompanyPhone:
+ bsvhu.destinationOperationNextDestinationCompanyPhone,
+ destinationOperationNextDestinationCompanySiret:
+ bsvhu.destinationOperationNextDestinationCompanySiret,
+ destinationOperationNextDestinationCompanyVatNumber:
+ bsvhu.destinationOperationNextDestinationCompanyVatNumber,
+ destinationOperationSignatureAuthor:
+ bsvhu.destinationOperationSignatureAuthor,
+ destinationOperationSignatureDate: bsvhu.destinationOperationSignatureDate,
+ destinationPlannedOperationCode: bsvhu.destinationPlannedOperationCode,
+ destinationReceptionAcceptationStatus:
+ bsvhu.destinationReceptionAcceptationStatus,
+ destinationReceptionDate: bsvhu.destinationReceptionDate,
+ destinationReceptionIdentificationNumbers:
+ bsvhu.destinationReceptionIdentificationNumbers,
+ destinationReceptionIdentificationType:
+ bsvhu.destinationReceptionIdentificationType,
+ destinationReceptionQuantity: bsvhu.destinationReceptionQuantity,
+ destinationReceptionRefusalReason: bsvhu.destinationReceptionRefusalReason,
+ destinationReceptionWeight: bsvhu.destinationReceptionWeight,
+ destinationType: bsvhu.destinationType,
+ emitterAgrementNumber: bsvhu.emitterAgrementNumber,
+ emitterCompanyAddress: bsvhu.emitterCompanyAddress,
+ emitterCompanyCity: bsvhu.emitterCompanyCity,
+ emitterCompanyContact: bsvhu.emitterCompanyContact,
+ emitterCompanyCountry: bsvhu.emitterCompanyCountry,
+ emitterCompanyMail: bsvhu.emitterCompanyMail,
+ emitterCompanyName: bsvhu.emitterCompanyName,
+ emitterCompanyPhone: bsvhu.emitterCompanyPhone,
+ emitterCompanyPostalCode: bsvhu.emitterCompanyPostalCode,
+ emitterCompanySiret: bsvhu.emitterCompanySiret,
+ emitterCompanyStreet: bsvhu.emitterCompanyStreet,
+ emitterCustomInfo: bsvhu.emitterCustomInfo,
+ emitterEmissionSignatureAuthor: bsvhu.emitterEmissionSignatureAuthor,
+ emitterEmissionSignatureDate: bsvhu.emitterEmissionSignatureDate,
+ emitterIrregularSituation: bsvhu.emitterIrregularSituation,
+ emitterNoSiret: bsvhu.emitterNoSiret,
+ identificationNumbers: bsvhu.identificationNumbers,
+ identificationType: bsvhu.identificationType,
+ intermediaries: bsvhu.intermediaries.length
+ ? {
+ createMany: {
+ data: bsvhu.intermediaries
+ }
+ }
+ : {},
+ intermediariesOrgIds: bsvhu.intermediariesOrgIds,
+ canAccessDraftOrgIds: bsvhu.canAccessDraftOrgIds,
+ isDeleted: bsvhu.isDeleted,
+ isDraft: bsvhu.isDraft,
+ packaging: bsvhu.packaging,
+ quantity: bsvhu.quantity,
+ status: bsvhu.status,
+ transporterCompanyAddress: bsvhu.transporterCompanyAddress,
+ transporterCompanyContact: bsvhu.transporterCompanyContact,
+ transporterCompanyMail: bsvhu.transporterCompanyMail,
+ transporterCompanyName: bsvhu.transporterCompanyName,
+ transporterCompanyPhone: bsvhu.transporterCompanyPhone,
+ transporterCompanySiret: bsvhu.transporterCompanySiret,
+ transporterCompanyVatNumber: bsvhu.transporterCompanyVatNumber,
+ transporterCustomInfo: bsvhu.transporterCustomInfo,
+ transporterRecepisseDepartment: bsvhu.transporterRecepisseDepartment,
+ transporterRecepisseIsExempted: bsvhu.transporterRecepisseIsExempted,
+ transporterRecepisseNumber: bsvhu.transporterRecepisseNumber,
+ transporterRecepisseValidityLimit: bsvhu.transporterRecepisseValidityLimit,
+ transporterTransportPlates: bsvhu.transporterTransportPlates,
+ transporterTransportSignatureAuthor:
+ bsvhu.transporterTransportSignatureAuthor,
+ transporterTransportSignatureDate: bsvhu.transporterTransportSignatureDate,
+ transporterTransportTakenOverAt: bsvhu.transporterTransportTakenOverAt,
+ brokerCompanyName: bsvhu.brokerCompanyName,
+ brokerCompanySiret: bsvhu.brokerCompanySiret,
+ brokerCompanyAddress: bsvhu.brokerCompanyAddress,
+ brokerCompanyContact: bsvhu.brokerCompanyContact,
+ brokerCompanyPhone: bsvhu.brokerCompanyPhone,
+ brokerCompanyMail: bsvhu.brokerCompanyMail,
+ brokerRecepisseNumber: bsvhu.brokerRecepisseNumber,
+ brokerRecepisseDepartment: bsvhu.brokerRecepisseDepartment,
+ brokerRecepisseValidityLimit: bsvhu.brokerRecepisseValidityLimit,
+ traderCompanyName: bsvhu.traderCompanyName,
+ traderCompanySiret: bsvhu.traderCompanySiret,
+ traderCompanyAddress: bsvhu.traderCompanyAddress,
+ traderCompanyContact: bsvhu.traderCompanyContact,
+ traderCompanyPhone: bsvhu.traderCompanyPhone,
+ traderCompanyMail: bsvhu.traderCompanyMail,
+ traderRecepisseNumber: bsvhu.traderRecepisseNumber,
+ traderRecepisseDepartment: bsvhu.traderRecepisseDepartment,
+ traderRecepisseValidityLimit: bsvhu.traderRecepisseValidityLimit,
+ updatedAt: bsvhu.updatedAt,
+ wasteCode: bsvhu.wasteCode,
+ weightIsEstimate: bsvhu.weightIsEstimate,
+ weightValue: bsvhu.weightValue,
+ ecoOrganismeName: bsvhu.ecoOrganismeName,
+ ecoOrganismeSiret: bsvhu.ecoOrganismeSiret
+ };
+
+ const newBsvhu = await create(newBsvhuCreateInput);
+
+ return newBsvhu;
+};
+
+export const bspaohInclude = {
+ transporters: true
+};
+
+export const cloneBspaoh = async (user: Express.User, id: string) => {
+ const bspaoh = await prisma.bspaoh.findFirstOrThrow({
+ where: { id },
+ include: bspaohInclude
+ });
+
+ if (!bspaoh) {
+ throw new UserInputError(`ID invalide ${id}`);
+ }
+
+ const { create } = getBspaohRepository(user);
+
+ const newBspaohCreateInput: Omit<
+ Required,
+ // Ignored for the time being
+ "rowNumber"
+ > = {
+ id: getReadableId(ReadableIdPrefix.PAOH),
+ canAccessDraftSirets: bspaoh.canAccessDraftSirets,
+ createdAt: bspaoh.createdAt,
+ currentTransporterOrgId: bspaoh.currentTransporterOrgId,
+ destinationCap: bspaoh.destinationCap,
+ destinationCompanyAddress: bspaoh.destinationCompanyAddress,
+ destinationCompanyContact: bspaoh.destinationCompanyContact,
+ destinationCompanyMail: bspaoh.destinationCompanyMail,
+ destinationCompanyName: bspaoh.destinationCompanyName,
+ destinationCompanyPhone: bspaoh.destinationCompanyPhone,
+ destinationCompanySiret: bspaoh.destinationCompanySiret,
+ destinationCustomInfo: bspaoh.destinationCustomInfo,
+ destinationOperationCode: bspaoh.destinationOperationCode,
+ destinationOperationDate: bspaoh.destinationOperationDate,
+ destinationOperationSignatureAuthor:
+ bspaoh.destinationOperationSignatureAuthor,
+ destinationOperationSignatureDate: bspaoh.destinationOperationSignatureDate,
+ destinationReceptionAcceptationStatus:
+ bspaoh.destinationReceptionAcceptationStatus,
+ destinationReceptionDate: bspaoh.destinationReceptionDate,
+ destinationReceptionSignatureAuthor:
+ bspaoh.destinationReceptionSignatureAuthor,
+ destinationReceptionSignatureDate: bspaoh.destinationReceptionSignatureDate,
+ destinationReceptionWasteAcceptedWeightValue:
+ bspaoh.destinationReceptionWasteAcceptedWeightValue,
+ destinationReceptionWastePackagingsAcceptation: prismaJsonNoNull(
+ bspaoh.destinationReceptionWastePackagingsAcceptation
+ ),
+ destinationReceptionWasteQuantityValue:
+ bspaoh.destinationReceptionWasteQuantityValue,
+ destinationReceptionWasteReceivedWeightValue:
+ bspaoh.destinationReceptionWasteReceivedWeightValue,
+ destinationReceptionWasteRefusalReason:
+ bspaoh.destinationReceptionWasteRefusalReason,
+ destinationReceptionWasteRefusedWeightValue:
+ bspaoh.destinationReceptionWasteRefusedWeightValue,
+ emitterCompanyAddress: bspaoh.emitterCompanyAddress,
+ emitterCompanyContact: bspaoh.emitterCompanyContact,
+ emitterCompanyMail: bspaoh.emitterCompanyMail,
+ emitterCompanyName: bspaoh.emitterCompanyName,
+ emitterCompanyPhone: bspaoh.emitterCompanyPhone,
+ emitterCompanySiret: bspaoh.emitterCompanySiret,
+ emitterCustomInfo: bspaoh.emitterCustomInfo,
+ emitterEmissionSignatureAuthor: bspaoh.emitterEmissionSignatureAuthor,
+ emitterEmissionSignatureDate: bspaoh.emitterEmissionSignatureDate,
+ emitterPickupSiteAddress: bspaoh.emitterPickupSiteAddress,
+ emitterPickupSiteCity: bspaoh.emitterPickupSiteCity,
+ emitterPickupSiteInfos: bspaoh.emitterPickupSiteInfos,
+ emitterPickupSiteName: bspaoh.emitterPickupSiteName,
+ emitterPickupSitePostalCode: bspaoh.emitterPickupSitePostalCode,
+ emitterWasteQuantityValue: bspaoh.emitterWasteQuantityValue,
+ emitterWasteWeightIsEstimate: bspaoh.emitterWasteWeightIsEstimate,
+ emitterWasteWeightValue: bspaoh.emitterWasteWeightValue,
+ handedOverToDestinationSignatureAuthor:
+ bspaoh.handedOverToDestinationSignatureAuthor,
+ handedOverToDestinationSignatureDate:
+ bspaoh.handedOverToDestinationSignatureDate,
+ isDeleted: bspaoh.isDeleted,
+ nextTransporterOrgId: bspaoh.nextTransporterOrgId,
+ status: bspaoh.status,
+ transporters: bspaoh.transporters.length
+ ? {
+ createMany: {
+ data: bspaoh.transporters!.map((t, idx) => {
+ const { id, ...data } = t;
+ return { ...data, bspaohId: undefined, number: idx + 1 };
+ })
+ }
+ }
+ : {},
+ transportersSirets: bspaoh.transportersSirets,
+ transporterTransportTakenOverAt: bspaoh.transporterTransportTakenOverAt,
+ updatedAt: bspaoh.updatedAt,
+ wasteAdr: bspaoh.wasteAdr,
+ wasteCode: bspaoh.wasteCode,
+ wastePackagings: prismaJsonNoNull(bspaoh.wastePackagings),
+ wasteType: bspaoh.wasteType
+ };
+
+ const newBspaoh = await create(newBspaohCreateInput);
+
+ return newBspaoh;
+};
+
+export const bsddInclude = {
+ transporters: true,
+ intermediaries: true,
+ finalOperations: true,
+ grouping: true,
+ groupedIn: true,
+ forwarding: true
+};
+
+export const cloneBsdd = async (
+ user: Express.User,
+ id: string
+): Promise
) : null}
@@ -107,6 +112,16 @@ export function BsvhuPdf({ bsvhu, qrCode, renderEmpty }: Props) {
Nom de la personne à contacter : {bsvhu.emitter?.company?.contact}
+ {bsvhu?.ecoOrganisme?.siret && (
+
+ Eco-organisme désigné : {" "}
+
+ Raison sociale : {bsvhu.ecoOrganisme?.name}
+
+ SIREN : {bsvhu.ecoOrganisme?.siret?.substring(0, 9)}
+
+
+ )}
{/* End Emitter */}
{/* Recipient */}
@@ -260,7 +275,7 @@ export function BsvhuPdf({ bsvhu, qrCode, renderEmpty }: Props) {
{/* End Waste identification*/}
- {/* Emitter signature*/}{" "}
+ {/* Emitter signature*/}
@@ -285,12 +300,80 @@ export function BsvhuPdf({ bsvhu, qrCode, renderEmpty }: Props) {
- {/* End Emitter signature*/}
+ {/* End Emitter signature */}
+ {/* Trader informations or broker information if no trader */}
+
+ {/* End Broker/Trader informations */}
+ {/* Broker information */}
+ {bsvhu.broker ? (
+
+ ) : null}
+ {/* End Broker informations */}
{/* Transporter */}
- 8. Transporteur
+ 9. Transporteur
@@ -354,7 +437,7 @@ export function BsvhuPdf({ bsvhu, qrCode, renderEmpty }: Props) {
-
10. Réalisation de l’opération
+
11. Réalisation de l’opération
-
Récépissé n° : {recepisse?.number}
-
-
Département : {recepisse?.department}
-
- Date limite de validité : {formatDate(recepisse?.validityLimit)}
-
-
- >
- );
-}
diff --git a/back/src/bsvhu/permissions.ts b/back/src/bsvhu/permissions.ts
index 852009b779..ab20d0e336 100644
--- a/back/src/bsvhu/permissions.ts
+++ b/back/src/bsvhu/permissions.ts
@@ -6,13 +6,18 @@ import { Permission, checkUserPermissions } from "../permissions";
* Retrieves organisations allowed to read a BSVHU
*/
function readers(bsvhu: Bsvhu): string[] {
- return [
- bsvhu.emitterCompanySiret,
- bsvhu.destinationCompanySiret,
- bsvhu.transporterCompanySiret,
- bsvhu.transporterCompanyVatNumber,
- ...bsvhu.intermediariesOrgIds
- ].filter(Boolean);
+ return bsvhu.isDraft
+ ? [...bsvhu.canAccessDraftOrgIds]
+ : [
+ bsvhu.emitterCompanySiret,
+ bsvhu.destinationCompanySiret,
+ bsvhu.transporterCompanySiret,
+ bsvhu.transporterCompanyVatNumber,
+ bsvhu.ecoOrganismeSiret,
+ bsvhu.brokerCompanySiret,
+ bsvhu.traderCompanySiret,
+ ...bsvhu.intermediariesOrgIds
+ ].filter(Boolean);
}
/**
@@ -22,11 +27,18 @@ function readers(bsvhu: Bsvhu): string[] {
* a user is not removing his own company from the BSVHU
*/
function contributors(bsvhu: Bsvhu, input?: BsvhuInput): string[] {
+ if (bsvhu.isDraft) {
+ return [...bsvhu.canAccessDraftOrgIds];
+ }
const updateEmitterCompanySiret = input?.emitter?.company?.siret;
const updateDestinationCompanySiret = input?.destination?.company?.siret;
const updateTransporterCompanySiret = input?.transporter?.company?.siret;
const updateTransporterCompanyVatNumber =
input?.transporter?.company?.vatNumber;
+ const updateEcoOrganismeCompanySiret = input?.ecoOrganisme?.siret;
+ const updateBrokerCompanySiret = input?.broker?.company?.siret;
+ const updateTraderCompanySiret = input?.trader?.company?.siret;
+
const updateIntermediaries = (input?.intermediaries ?? []).flatMap(i => [
i.siret,
i.vatNumber
@@ -52,6 +64,21 @@ function contributors(bsvhu: Bsvhu, input?: BsvhuInput): string[] {
? updateTransporterCompanyVatNumber
: bsvhu.transporterCompanyVatNumber;
+ const ecoOrganismeCompanySiret =
+ updateEcoOrganismeCompanySiret !== undefined
+ ? updateEcoOrganismeCompanySiret
+ : bsvhu.ecoOrganismeSiret;
+
+ const brokerCompanySiret =
+ updateBrokerCompanySiret !== undefined
+ ? updateBrokerCompanySiret
+ : bsvhu.brokerCompanySiret;
+
+ const traderCompanySiret =
+ updateTraderCompanySiret !== undefined
+ ? updateTraderCompanySiret
+ : bsvhu.traderCompanySiret;
+
const intermediariesOrgIds =
input?.intermediaries !== undefined
? updateIntermediaries
@@ -62,6 +89,9 @@ function contributors(bsvhu: Bsvhu, input?: BsvhuInput): string[] {
destinationCompanySiret,
transporterCompanySiret,
transporterCompanyVatNumber,
+ ecoOrganismeCompanySiret,
+ brokerCompanySiret,
+ traderCompanySiret,
...intermediariesOrgIds
].filter(Boolean);
}
@@ -72,9 +102,12 @@ function contributors(bsvhu: Bsvhu, input?: BsvhuInput): string[] {
function creators(input: BsvhuInput) {
return [
input.emitter?.company?.siret,
+ input.ecoOrganisme?.siret,
input.transporter?.company?.siret,
input.transporter?.company?.vatNumber,
- input.destination?.company?.siret
+ input.destination?.company?.siret,
+ input.broker?.company?.siret,
+ input.trader?.company?.siret
].filter(Boolean);
}
diff --git a/back/src/bsvhu/registry.ts b/back/src/bsvhu/registry.ts
index fc6b3cc5e4..861a775c78 100644
--- a/back/src/bsvhu/registry.ts
+++ b/back/src/bsvhu/registry.ts
@@ -119,6 +119,18 @@ export function getRegistryFields(
registryFields.isTransportedWasteFor.push(bsvhu.transporterCompanySiret);
registryFields.isAllWasteFor.push(bsvhu.transporterCompanySiret);
}
+ if (bsvhu.ecoOrganismeSiret) {
+ registryFields.isOutgoingWasteFor.push(bsvhu.ecoOrganismeSiret);
+ registryFields.isAllWasteFor.push(bsvhu.ecoOrganismeSiret);
+ }
+ if (bsvhu.brokerCompanySiret) {
+ registryFields.isManagedWasteFor.push(bsvhu.brokerCompanySiret);
+ registryFields.isAllWasteFor.push(bsvhu.brokerCompanySiret);
+ }
+ if (bsvhu.traderCompanySiret) {
+ registryFields.isManagedWasteFor.push(bsvhu.traderCompanySiret);
+ registryFields.isAllWasteFor.push(bsvhu.traderCompanySiret);
+ }
if (bsvhu.intermediaries?.length) {
for (const intermediary of bsvhu.intermediaries) {
const intermediaryOrgId = getIntermediaryCompanyOrgId(intermediary);
@@ -172,8 +184,8 @@ export function toGenericWaste(bsvhu: RegistryBsvhu): GenericWaste {
id: bsvhu.id,
createdAt: bsvhu.createdAt,
updatedAt: bsvhu.createdAt,
- ecoOrganismeName: null,
- ecoOrganismeSiren: null,
+ ecoOrganismeName: bsvhu.ecoOrganismeName,
+ ecoOrganismeSiren: bsvhu.ecoOrganismeSiret?.slice(0, 9),
bsdType: "BSVHU",
bsdSubType: getBsvhuSubType(bsvhu),
status: bsvhu.status,
@@ -215,6 +227,8 @@ export function toGenericWaste(bsvhu: RegistryBsvhu): GenericWaste {
emitterCompanyName: bsvhu.emitterCompanyName,
emitterCompanySiret: bsvhu.emitterCompanySiret,
emitterCompanyIrregularSituation: !!bsvhu.emitterIrregularSituation,
+ brokerCompanyMail: bsvhu.brokerCompanyMail,
+ traderCompanyMail: bsvhu.traderCompanyMail,
weight: bsvhu.weightValue
? new Decimal(bsvhu.weightValue)
.dividedBy(1000)
@@ -233,12 +247,12 @@ export function toIncomingWaste(bsvhu: RegistryBsvhu): Required
{
...emptyIncomingWaste,
...genericWaste,
destinationReceptionDate: bsvhu.destinationReceptionDate,
- traderCompanyName: null,
- traderCompanySiret: null,
- traderRecepisseNumber: null,
- brokerCompanyName: null,
- brokerCompanySiret: null,
- brokerRecepisseNumber: null,
+ traderCompanyName: bsvhu.traderCompanyName,
+ traderCompanySiret: bsvhu.traderCompanySiret,
+ traderRecepisseNumber: bsvhu.traderRecepisseNumber,
+ brokerCompanyName: bsvhu.brokerCompanyName,
+ brokerCompanySiret: bsvhu.brokerCompanySiret,
+ brokerRecepisseNumber: bsvhu.brokerRecepisseNumber,
emitterCompanyMail: bsvhu.emitterCompanyMail,
...getOperationData(bsvhu),
...getTransporterData(bsvhu),
@@ -253,13 +267,13 @@ export function toOutgoingWaste(bsvhu: RegistryBsvhu): Required {
// Make sure all possible keys are in the exported sheet so that no column is missing
...emptyOutgoingWaste,
...genericWaste,
- brokerCompanyName: null,
- brokerCompanySiret: null,
- brokerRecepisseNumber: null,
destinationPlannedOperationMode: null,
- traderCompanyName: null,
- traderCompanySiret: null,
- traderRecepisseNumber: null,
+ brokerCompanyName: bsvhu.brokerCompanyName,
+ brokerCompanySiret: bsvhu.brokerCompanySiret,
+ brokerRecepisseNumber: bsvhu.brokerRecepisseNumber,
+ traderCompanyName: bsvhu.traderCompanyName,
+ traderCompanySiret: bsvhu.traderCompanySiret,
+ traderRecepisseNumber: bsvhu.traderRecepisseNumber,
...getOperationData(bsvhu),
weight: bsvhu.weightValue ? bsvhu.weightValue / 1000 : bsvhu.weightValue,
...getOperationData(bsvhu),
@@ -279,12 +293,12 @@ export function toTransportedWaste(
...genericWaste,
destinationReceptionDate: bsvhu.destinationReceptionDate,
weight: bsvhu.weightValue ? bsvhu.weightValue / 1000 : bsvhu.weightValue,
- traderCompanyName: null,
- traderCompanySiret: null,
- traderRecepisseNumber: null,
- brokerCompanyName: null,
- brokerCompanySiret: null,
- brokerRecepisseNumber: null,
+ traderCompanyName: bsvhu.traderCompanyName,
+ traderCompanySiret: bsvhu.traderCompanySiret,
+ traderRecepisseNumber: bsvhu.traderRecepisseNumber,
+ brokerCompanyName: bsvhu.brokerCompanyName,
+ brokerCompanySiret: bsvhu.brokerCompanySiret,
+ brokerRecepisseNumber: bsvhu.brokerRecepisseNumber,
emitterCompanyMail: bsvhu.emitterCompanyMail,
...getTransporterData(bsvhu, true)
};
@@ -301,10 +315,10 @@ export function toManagedWaste(bsvhu: RegistryBsvhu): Required {
// Make sure all possible keys are in the exported sheet so that no column is missing
...emptyManagedWaste,
...genericWaste,
- traderCompanyName: null,
- traderCompanySiret: null,
- brokerCompanyName: null,
- brokerCompanySiret: null,
+ traderCompanyName: bsvhu.traderCompanyName,
+ traderCompanySiret: bsvhu.traderCompanySiret,
+ brokerCompanyName: bsvhu.brokerCompanyName,
+ brokerCompanySiret: bsvhu.brokerCompanySiret,
destinationPlannedOperationMode: null,
emitterCompanyMail: bsvhu.emitterCompanyMail,
...getTransporterData(bsvhu)
@@ -320,14 +334,14 @@ export function toAllWaste(bsvhu: RegistryBsvhu): Required {
...genericWaste,
createdAt: bsvhu.createdAt,
destinationReceptionDate: bsvhu.destinationReceptionDate,
- brokerCompanyName: null,
- brokerCompanySiret: null,
- brokerRecepisseNumber: null,
+ brokerCompanyName: bsvhu.brokerCompanyName,
+ brokerCompanySiret: bsvhu.brokerCompanySiret,
+ brokerRecepisseNumber: bsvhu.brokerRecepisseNumber,
+ traderCompanyName: bsvhu.traderCompanyName,
+ traderCompanySiret: bsvhu.traderCompanySiret,
+ traderRecepisseNumber: bsvhu.traderRecepisseNumber,
destinationPlannedOperationMode: null,
weight: bsvhu.weightValue ? bsvhu.weightValue / 1000 : bsvhu.weightValue,
- traderCompanyName: null,
- traderCompanySiret: null,
- traderRecepisseNumber: null,
emitterCompanyMail: bsvhu.emitterCompanyMail,
...getOperationData(bsvhu),
...getTransporterData(bsvhu, true),
diff --git a/back/src/bsvhu/repository/bsvhu/create.ts b/back/src/bsvhu/repository/bsvhu/create.ts
index 96eb815a17..3719cd835c 100644
--- a/back/src/bsvhu/repository/bsvhu/create.ts
+++ b/back/src/bsvhu/repository/bsvhu/create.ts
@@ -5,6 +5,7 @@ import {
} from "../../../common/repository/types";
import { enqueueCreatedBsdToIndex } from "../../../queue/producers/elastic";
import { bsvhuEventTypes } from "./eventTypes";
+import { getCanAccessDraftOrgIds } from "../../utils";
export type CreateBsvhuFn = (
data: Prisma.BsvhuCreateInput,
logMetadata?: LogMetadata
@@ -14,7 +15,12 @@ export function buildCreateBsvhu(deps: RepositoryFnDeps): CreateBsvhuFn {
return async (data, logMetadata?) => {
const { prisma, user } = deps;
- const bsvhu = await prisma.bsvhu.create({ data });
+ const bsvhu = await prisma.bsvhu.create({
+ data,
+ include: {
+ intermediaries: true
+ }
+ });
await prisma.event.create({
data: {
@@ -26,8 +32,23 @@ export function buildCreateBsvhu(deps: RepositoryFnDeps): CreateBsvhuFn {
}
});
- prisma.addAfterCommitCallback(() => enqueueCreatedBsdToIndex(bsvhu.id));
+ // For drafts, only the owner's sirets that appear on the bsd have access
+ const canAccessDraftOrgIds = await getCanAccessDraftOrgIds(bsvhu, user.id);
+
+ const updatedBsvhu = await prisma.bsvhu.update({
+ where: { id: bsvhu.id },
+ data: {
+ ...(canAccessDraftOrgIds.length ? { canAccessDraftOrgIds } : {})
+ },
+ include: {
+ intermediaries: true
+ }
+ });
+
+ prisma.addAfterCommitCallback(() =>
+ enqueueCreatedBsdToIndex(updatedBsvhu.id)
+ );
- return bsvhu;
+ return updatedBsvhu;
};
}
diff --git a/back/src/bsvhu/resolvers/mutations/__tests__/createBsvhu.integration.ts b/back/src/bsvhu/resolvers/mutations/__tests__/createBsvhu.integration.ts
index 3314fe114f..8826480b86 100644
--- a/back/src/bsvhu/resolvers/mutations/__tests__/createBsvhu.integration.ts
+++ b/back/src/bsvhu/resolvers/mutations/__tests__/createBsvhu.integration.ts
@@ -6,7 +6,8 @@ import {
siretify,
userFactory,
userWithCompanyFactory,
- transporterReceiptFactory
+ transporterReceiptFactory,
+ ecoOrganismeFactory
} from "../../../../__tests__/factories";
import makeClient from "../../../../__tests__/testClient";
import gql from "graphql-tag";
@@ -46,6 +47,10 @@ const CREATE_VHU_FORM = gql`
intermediaries {
siret
}
+ ecoOrganisme {
+ siret
+ name
+ }
weight {
value
}
@@ -110,7 +115,8 @@ describe("Mutation.Vhu.create", () => {
it("should allow creating a valid form for the producer signature", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
const input = {
@@ -171,7 +177,8 @@ describe("Mutation.Vhu.create", () => {
it("should create a bsvhu and autocomplete transporter recepisse", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
const transporter = await companyFactory({
@@ -239,7 +246,8 @@ describe("Mutation.Vhu.create", () => {
it("should create a bsvhu and ignore recepisse input", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
const transporter = await companyFactory({
@@ -311,10 +319,77 @@ describe("Mutation.Vhu.create", () => {
);
});
+ it("should create a bsvhu with eco-organisme", async () => {
+ const { user, company } = await userWithCompanyFactory("MEMBER");
+ const destinationCompany = await companyFactory({
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
+ });
+
+ const ecoOrganisme = await ecoOrganismeFactory({
+ handle: { handleBsvhu: true },
+ createAssociatedCompany: true
+ });
+
+ const input = {
+ emitter: {
+ company: {
+ siret: company.siret,
+ name: "The crusher",
+ address: "Rue de la carcasse",
+ contact: "Un centre VHU",
+ phone: "0101010101",
+ mail: "emitter@mail.com"
+ },
+ agrementNumber: "1234"
+ },
+ wasteCode: "16 01 06",
+ packaging: "UNITE",
+ identification: {
+ numbers: ["123", "456"],
+ type: "NUMERO_ORDRE_REGISTRE_POLICE"
+ },
+ quantity: 2,
+ weight: {
+ isEstimate: false,
+ value: 1.3
+ },
+ destination: {
+ type: "BROYEUR",
+ plannedOperationCode: "R 12",
+ company: {
+ siret: destinationCompany.siret,
+ name: "destination",
+ address: "address",
+ contact: "contactEmail",
+ phone: "contactPhone",
+ mail: "contactEmail@mail.com"
+ },
+ agrementNumber: "9876"
+ },
+ ecoOrganisme: {
+ siret: ecoOrganisme.siret,
+ name: ecoOrganisme.name
+ }
+ };
+ const { mutate } = makeClient(user);
+ const { data } = await mutate>(
+ CREATE_VHU_FORM,
+ {
+ variables: {
+ input
+ }
+ }
+ );
+ expect(data.createBsvhu.id).toBeDefined();
+ expect(data.createBsvhu.ecoOrganisme!.siret).toBe(ecoOrganisme.siret);
+ });
+
it("should create a bsvhu with intermediary", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
const intermediary = await companyFactory({
@@ -382,7 +457,8 @@ describe("Mutation.Vhu.create", () => {
it("should fail if creating a bsvhu with the same intermediary several times", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
const intermediary = await companyFactory({
@@ -457,7 +533,8 @@ describe("Mutation.Vhu.create", () => {
it("should fail if creating a bsvhu with more than 3 intermediaries", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
const intermediary1 = await companyFactory({
@@ -553,7 +630,8 @@ describe("Mutation.Vhu.create", () => {
it("should fail if a required field like the recipient agrement is missing", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
const input = {
@@ -619,7 +697,8 @@ describe("Mutation.Vhu.create", () => {
}
);
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
await transporterReceiptFactory({
@@ -687,7 +766,8 @@ describe("Mutation.Vhu.create", () => {
}
);
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
await transporterReceiptFactory({
@@ -752,7 +832,8 @@ describe("Mutation.Vhu.create", () => {
}
);
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
});
await transporterReceiptFactory({
diff --git a/back/src/bsvhu/resolvers/mutations/__tests__/duplicateBsvhu.integration.ts b/back/src/bsvhu/resolvers/mutations/__tests__/duplicateBsvhu.integration.ts
index 832656319c..2ece1440b7 100644
--- a/back/src/bsvhu/resolvers/mutations/__tests__/duplicateBsvhu.integration.ts
+++ b/back/src/bsvhu/resolvers/mutations/__tests__/duplicateBsvhu.integration.ts
@@ -3,6 +3,7 @@ import { xDaysAgo } from "../../../../utils";
import { resetDatabase } from "../../../../../integration-tests/helper";
import {
companyFactory,
+ ecoOrganismeFactory,
transporterReceiptFactory,
userWithCompanyFactory
} from "../../../../__tests__/factories";
@@ -78,11 +79,47 @@ describe("mutaion.duplicateBsvhu", () => {
await prisma.transporterReceipt.findUniqueOrThrow({
where: { id: transporter.transporterReceiptId! }
});
- const destination = await companyFactory();
+ const destination = await companyFactory({
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
+ });
const intermediary = await companyFactory();
+ const ecoOrganisme = await ecoOrganismeFactory({
+ handle: { handleBsvhu: true },
+ createAssociatedCompany: true
+ });
+
+ const broker = await companyFactory({
+ companyTypes: ["BROKER"],
+ brokerReceipt: {
+ create: {
+ receiptNumber: "BROKER-RECEIPT-NUMBER",
+ validityLimit: TODAY.toISOString() as any,
+ department: "BROKER-RECEIPT-DEPARTMENT"
+ }
+ }
+ });
+ const brokerReceipt = await prisma.brokerReceipt.findUniqueOrThrow({
+ where: { id: broker.brokerReceiptId! }
+ });
+ const trader = await companyFactory({
+ companyTypes: ["TRADER"],
+ traderReceipt: {
+ create: {
+ receiptNumber: "TRADER-RECEIPT-NUMBER",
+ validityLimit: TODAY.toISOString() as any,
+ department: "TRADER-RECEIPT-DEPARTMENT"
+ }
+ }
+ });
+ const traderReceipt = await prisma.traderReceipt.findUniqueOrThrow({
+ where: { id: trader.traderReceiptId! }
+ });
+
const bsvhu = await bsvhuFactory({
+ userId: emitter.user.id,
opt: {
emitterIrregularSituation: false,
emitterNoSiret: false,
@@ -125,7 +162,27 @@ describe("mutaion.duplicateBsvhu", () => {
}
]
}
- }
+ },
+ ecoOrganismeSiret: ecoOrganisme.siret,
+ ecoOrganismeName: ecoOrganisme.name,
+ brokerCompanyName: broker.name,
+ brokerCompanySiret: broker.siret,
+ brokerCompanyAddress: broker.address,
+ brokerCompanyContact: broker.contact,
+ brokerCompanyPhone: broker.contactPhone,
+ brokerCompanyMail: broker.contactEmail,
+ brokerRecepisseNumber: brokerReceipt.receiptNumber,
+ brokerRecepisseDepartment: brokerReceipt.department,
+ brokerRecepisseValidityLimit: brokerReceipt.validityLimit,
+ traderCompanyName: trader.name,
+ traderCompanySiret: trader.siret,
+ traderCompanyAddress: trader.address,
+ traderCompanyContact: trader.contact,
+ traderCompanyPhone: trader.contactPhone,
+ traderCompanyMail: trader.contactEmail,
+ traderRecepisseNumber: traderReceipt.receiptNumber,
+ traderRecepisseDepartment: traderReceipt.department,
+ traderRecepisseValidityLimit: traderReceipt.validityLimit
}
});
const { mutate } = makeClient(emitter.user);
@@ -195,6 +252,26 @@ describe("mutaion.duplicateBsvhu", () => {
transporterCustomInfo,
transporterTransportPlates,
transporterRecepisseIsExempted,
+ ecoOrganismeSiret,
+ ecoOrganismeName,
+ brokerCompanyName,
+ brokerCompanySiret,
+ brokerCompanyAddress,
+ brokerCompanyContact,
+ brokerCompanyPhone,
+ brokerCompanyMail,
+ brokerRecepisseNumber,
+ brokerRecepisseDepartment,
+ brokerRecepisseValidityLimit,
+ traderCompanyName,
+ traderCompanySiret,
+ traderCompanyAddress,
+ traderCompanyContact,
+ traderCompanyPhone,
+ traderCompanyMail,
+ traderRecepisseNumber,
+ traderRecepisseDepartment,
+ traderRecepisseValidityLimit,
...rest
} = bsvhu;
@@ -225,7 +302,8 @@ describe("mutaion.duplicateBsvhu", () => {
"destinationOperationSignatureDate",
"intermediaries",
- "intermediariesOrgIds"
+ "intermediariesOrgIds",
+ "canAccessDraftOrgIds"
];
expect(duplicatedBsvhu.status).toEqual("INITIAL");
@@ -283,7 +361,27 @@ describe("mutaion.duplicateBsvhu", () => {
transporterTransportTakenOverAt,
transporterCustomInfo,
transporterTransportPlates,
- transporterRecepisseIsExempted
+ transporterRecepisseIsExempted,
+ ecoOrganismeSiret,
+ ecoOrganismeName,
+ brokerCompanyName,
+ brokerCompanySiret,
+ brokerCompanyAddress,
+ brokerCompanyContact,
+ brokerCompanyPhone,
+ brokerCompanyMail,
+ brokerRecepisseNumber,
+ brokerRecepisseDepartment,
+ brokerRecepisseValidityLimit,
+ traderCompanyName,
+ traderCompanySiret,
+ traderCompanyAddress,
+ traderCompanyContact,
+ traderCompanyPhone,
+ traderCompanyMail,
+ traderRecepisseNumber,
+ traderRecepisseDepartment,
+ traderRecepisseValidityLimit
});
// make sure this test breaks when a new field is added to the Bsvhu model
@@ -374,6 +472,8 @@ describe("mutaion.duplicateBsvhu", () => {
where: { id: transporterCompany.transporterReceiptId! }
});
const destinationCompany = await companyFactory({
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"],
vhuAgrementDemolisseur: {
create: {
agrementNumber: "UPDATED-AGREEMENT-NUMBER",
@@ -558,6 +658,8 @@ describe("mutaion.duplicateBsvhu", () => {
where: { id: transporterCompany.transporterReceiptId! }
});
const destinationCompany = await companyFactory({
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"],
vhuAgrementDemolisseur: {
create: {
agrementNumber: "UPDATED-AGREEMENT-NUMBER",
diff --git a/back/src/bsvhu/resolvers/mutations/__tests__/publishBsvhu.integration.ts b/back/src/bsvhu/resolvers/mutations/__tests__/publishBsvhu.integration.ts
index 5c6de34445..4f4f773be0 100644
--- a/back/src/bsvhu/resolvers/mutations/__tests__/publishBsvhu.integration.ts
+++ b/back/src/bsvhu/resolvers/mutations/__tests__/publishBsvhu.integration.ts
@@ -63,6 +63,7 @@ describe("Mutation.Vhu.publish", () => {
it("should pass the form as non draft", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const form = await bsvhuFactory({
+ userId: user.id,
opt: {
emitterCompanySiret: company.siret,
isDraft: true
@@ -79,4 +80,40 @@ describe("Mutation.Vhu.publish", () => {
expect(data.publishBsvhu.isDraft).toBe(false);
});
+
+ it("should fail if the user is not part a the creator's companies", async () => {
+ const { user, company } = await userWithCompanyFactory("MEMBER");
+ const { company: company2, user: user2 } = await userWithCompanyFactory(
+ "ADMIN",
+ {
+ companyTypes: ["TRANSPORTER"]
+ }
+ );
+ const form = await bsvhuFactory({
+ userId: user.id,
+ opt: {
+ emitterCompanySiret: company.siret,
+ transporterCompanySiret: company2.siret,
+ isDraft: true
+ }
+ });
+
+ const { mutate } = makeClient(user2);
+ const { errors } = await mutate>(
+ PUBLISH_VHU_FORM,
+ {
+ variables: { id: form.id }
+ }
+ );
+
+ expect(errors).toEqual([
+ expect.objectContaining({
+ message:
+ "Vous ne pouvez pas modifier un bordereau sur lequel votre entreprise n'apparait pas",
+ extensions: expect.objectContaining({
+ code: ErrorCode.FORBIDDEN
+ })
+ })
+ ]);
+ });
});
diff --git a/back/src/bsvhu/resolvers/mutations/__tests__/signBsvhu.integration.ts b/back/src/bsvhu/resolvers/mutations/__tests__/signBsvhu.integration.ts
index b411ef4620..7d7667269e 100644
--- a/back/src/bsvhu/resolvers/mutations/__tests__/signBsvhu.integration.ts
+++ b/back/src/bsvhu/resolvers/mutations/__tests__/signBsvhu.integration.ts
@@ -7,7 +7,11 @@ import {
} from "../../../../__tests__/factories";
import makeClient from "../../../../__tests__/testClient";
import { bsvhuFactory } from "../../../__tests__/factories.vhu";
-import { companyFactory } from "../../../../__tests__/factories";
+import {
+ companyFactory,
+ transporterReceiptFactory
+} from "../../../../__tests__/factories";
+import { prisma } from "@td/prisma";
const SIGN_VHU_FORM = `
mutation SignVhuForm($id: ID!, $input: BsvhuSignatureInput!) {
@@ -74,6 +78,229 @@ describe("Mutation.Vhu.sign", () => {
expect(data.signBsvhu.emitter!.emission!.signature!.author).toBe(user.name);
expect(data.signBsvhu.emitter!.emission!.signature!.date).not.toBeNull();
});
+ it("should forbid another company to sign EMISSION when security code is not provided", async () => {
+ const { company: emitterCompany } = await userWithCompanyFactory("MEMBER", {
+ securityCode: 9421
+ });
+ const { user, company } = await userWithCompanyFactory("MEMBER");
+ const bsvhu = await bsvhuFactory({
+ opt: {
+ emitterCompanySiret: emitterCompany.siret,
+ destinationCompanySiret: company.siret
+ }
+ });
+
+ const { mutate } = makeClient(user);
+ const { errors } = await mutate>(
+ SIGN_VHU_FORM,
+ {
+ variables: {
+ id: bsvhu.id,
+ input: { type: "EMISSION", author: user.name }
+ }
+ }
+ );
+ expect(errors).toEqual([
+ expect.objectContaining({
+ message: "Vous ne pouvez pas signer ce bordereau",
+ extensions: expect.objectContaining({
+ code: ErrorCode.FORBIDDEN
+ })
+ })
+ ]);
+ });
+
+ it("should forbid another company to sign EMISSION when security code is wrong", async () => {
+ const { company: emitterCompany } = await userWithCompanyFactory("MEMBER", {
+ securityCode: 9421
+ });
+ const { user, company } = await userWithCompanyFactory("MEMBER");
+ const bsvhu = await bsvhuFactory({
+ opt: {
+ emitterCompanySiret: emitterCompany.siret,
+ destinationCompanySiret: company.siret
+ }
+ });
+
+ const { mutate } = makeClient(user);
+ const { errors } = await mutate>(
+ SIGN_VHU_FORM,
+ {
+ variables: {
+ id: bsvhu.id,
+ input: { type: "EMISSION", author: user.name, securityCode: 5555 }
+ }
+ }
+ );
+ expect(errors).toEqual([
+ expect.objectContaining({
+ message: "Le code de signature est invalide.",
+ extensions: expect.objectContaining({
+ code: ErrorCode.FORBIDDEN
+ })
+ })
+ ]);
+ });
+
+ it("should allow another company to sign EMISSION when security code is provided", async () => {
+ const { company: emitterCompany } = await userWithCompanyFactory("MEMBER", {
+ securityCode: 9421
+ });
+ const { user, company } = await userWithCompanyFactory("MEMBER", {
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
+ });
+ const bsvhu = await bsvhuFactory({
+ opt: {
+ emitterCompanySiret: emitterCompany.siret,
+ destinationCompanySiret: company.siret
+ }
+ });
+
+ const { mutate } = makeClient(user);
+ const { data } = await mutate>(SIGN_VHU_FORM, {
+ variables: {
+ id: bsvhu.id,
+ input: { type: "EMISSION", author: user.name, securityCode: 9421 }
+ }
+ });
+
+ expect(data.signBsvhu.emitter!.emission!.signature!.author).toBe(user.name);
+ expect(data.signBsvhu.emitter!.emission!.signature!.date).not.toBeNull();
+ });
+
+ it("should forbid another company to sign TRANSPORT when security code is not provided", async () => {
+ const { user: emitter, company: emitterCompany } =
+ await userWithCompanyFactory("MEMBER");
+ const { company: transporterCompany } = await userWithCompanyFactory(
+ "MEMBER",
+ {
+ companyTypes: ["TRANSPORTER"],
+ securityCode: 9421
+ }
+ );
+
+ await transporterReceiptFactory({ company: transporterCompany });
+ const bsvhu = await bsvhuFactory({
+ opt: {
+ emitterCompanySiret: emitterCompany.siret,
+ transporterCompanySiret: transporterCompany.siret,
+ status: "SIGNED_BY_PRODUCER"
+ }
+ });
+
+ const { mutate } = makeClient(emitter);
+ const { errors } = await mutate>(
+ SIGN_VHU_FORM,
+ {
+ variables: {
+ id: bsvhu.id,
+ input: { type: "TRANSPORT", author: emitter.name }
+ }
+ }
+ );
+
+ const signedBsvhu = await prisma.bsvhu.findUnique({
+ where: { id: bsvhu.id }
+ });
+ expect(signedBsvhu?.status).toEqual("SIGNED_BY_PRODUCER");
+
+ expect(errors).toEqual([
+ expect.objectContaining({
+ message: "Vous ne pouvez pas signer ce bordereau",
+ extensions: expect.objectContaining({
+ code: ErrorCode.FORBIDDEN
+ })
+ })
+ ]);
+ });
+
+ it("should forbid another company to sign TRANSPORT when security code is wrong", async () => {
+ const { user: emitter, company: emitterCompany } =
+ await userWithCompanyFactory("MEMBER");
+ const { company: transporterCompany } = await userWithCompanyFactory(
+ "MEMBER",
+ {
+ companyTypes: ["TRANSPORTER"],
+ securityCode: 9421
+ }
+ );
+
+ await transporterReceiptFactory({ company: transporterCompany });
+ const bsvhu = await bsvhuFactory({
+ opt: {
+ emitterCompanySiret: emitterCompany.siret,
+ transporterCompanySiret: transporterCompany.siret,
+ status: "SIGNED_BY_PRODUCER"
+ }
+ });
+
+ const { mutate } = makeClient(emitter);
+ const { errors } = await mutate>(
+ SIGN_VHU_FORM,
+ {
+ variables: {
+ id: bsvhu.id,
+ input: { type: "TRANSPORT", author: emitter.name, securityCode: 3333 }
+ }
+ }
+ );
+
+ const signedBsvhu = await prisma.bsvhu.findUnique({
+ where: { id: bsvhu.id }
+ });
+ expect(signedBsvhu?.status).toEqual("SIGNED_BY_PRODUCER");
+
+ expect(errors).toEqual([
+ expect.objectContaining({
+ message: "Le code de signature est invalide.",
+ extensions: expect.objectContaining({
+ code: ErrorCode.FORBIDDEN
+ })
+ })
+ ]);
+ });
+
+ it("should allow another company to sign TRANSPORT when security code is provided", async () => {
+ const { user: emitter, company: emitterCompany } =
+ await userWithCompanyFactory("MEMBER", {});
+ const { company: transporterCompany } = await userWithCompanyFactory(
+ "MEMBER",
+ {
+ companyTypes: ["TRANSPORTER"],
+ securityCode: 9421
+ }
+ );
+
+ await transporterReceiptFactory({ company: transporterCompany });
+ const bsvhu = await bsvhuFactory({
+ opt: {
+ emitterCompanySiret: emitterCompany.siret,
+ transporterCompanySiret: transporterCompany.siret,
+ status: "SIGNED_BY_PRODUCER"
+ }
+ });
+
+ const { mutate } = makeClient(emitter);
+ const { data } = await mutate>(SIGN_VHU_FORM, {
+ variables: {
+ id: bsvhu.id,
+ input: { type: "TRANSPORT", author: emitter.name, securityCode: 9421 }
+ }
+ });
+
+ const signedBsvhu = await prisma.bsvhu.findUnique({
+ where: { id: bsvhu.id }
+ });
+ expect(signedBsvhu?.status).toEqual("SENT");
+
+ expect(data.signBsvhu.transporter!.transport!.signature!.author).toBe(
+ emitter.name
+ );
+ expect(
+ data.signBsvhu.transporter!.transport!.signature!.date
+ ).not.toBeNull();
+ });
it("should use the provided date for the signature if given", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
diff --git a/back/src/bsvhu/resolvers/mutations/__tests__/updateBsvhu.integration.ts b/back/src/bsvhu/resolvers/mutations/__tests__/updateBsvhu.integration.ts
index 489ca915f0..e5ad991543 100644
--- a/back/src/bsvhu/resolvers/mutations/__tests__/updateBsvhu.integration.ts
+++ b/back/src/bsvhu/resolvers/mutations/__tests__/updateBsvhu.integration.ts
@@ -10,6 +10,7 @@ import {
import makeClient from "../../../../__tests__/testClient";
import { Mutation } from "../../../../generated/graphql/types";
import { UserRole } from "@prisma/client";
+import { prisma } from "@td/prisma";
import gql from "graphql-tag";
const UPDATE_VHU_FORM = gql`
@@ -112,6 +113,73 @@ describe("Mutation.Vhu.update", () => {
]);
});
+ it("should allow user to update a draft BSVHU", async () => {
+ const { company, user } = await userWithCompanyFactory(UserRole.ADMIN);
+ const bsvhu = await bsvhuFactory({
+ userId: user.id,
+ opt: {
+ isDraft: true,
+ status: "INITIAL",
+ emitterCompanySiret: company.siret
+ }
+ });
+ const { mutate } = makeClient(user);
+ const input = {
+ quantity: 4
+ };
+ const { errors } = await mutate>(
+ UPDATE_VHU_FORM,
+ {
+ variables: { id: bsvhu.id, input }
+ }
+ );
+ expect(errors).toBeUndefined();
+ const updatedBsvhu = await prisma.bsvhu.findFirstOrThrow({
+ where: { id: bsvhu.id }
+ });
+ expect(updatedBsvhu.quantity).toEqual(4);
+ });
+
+ it("should disallow user who isn't part of the creator's companies to update a draft BSVHU", async () => {
+ const { company, user } = await userWithCompanyFactory(UserRole.ADMIN);
+ const { company: company2, user: user2 } = await userWithCompanyFactory(
+ UserRole.ADMIN,
+ {
+ companyTypes: ["TRANSPORTER"]
+ }
+ );
+ const bsvhu = await bsvhuFactory({
+ userId: user.id,
+ opt: {
+ isDraft: true,
+ status: "INITIAL",
+ emitterCompanySiret: company.siret,
+ transporterCompanySiret: company2.siret
+ }
+ });
+ const { mutate } = makeClient(user2);
+ const input = {
+ weight: {
+ value: 4
+ }
+ };
+ const { errors } = await mutate>(
+ UPDATE_VHU_FORM,
+ {
+ variables: { id: bsvhu.id, input }
+ }
+ );
+ expect(errors).toEqual([
+ expect.objectContaining({
+ message:
+ "Vous ne pouvez pas modifier un bordereau sur lequel votre entreprise n'apparait pas",
+ extensions: expect.objectContaining({
+ code: ErrorCode.FORBIDDEN
+ })
+ })
+ ]);
+ });
+
it("should be possible to update a non signed form", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const form = await bsvhuFactory({
@@ -132,7 +200,6 @@ describe("Mutation.Vhu.update", () => {
variables: { id: form.id, input }
}
);
-
expect(data.updateBsvhu.weight!.value).toBe(4);
});
diff --git a/back/src/bsvhu/resolvers/mutations/duplicate.ts b/back/src/bsvhu/resolvers/mutations/duplicate.ts
index e64fdc6de3..c5906b99f3 100644
--- a/back/src/bsvhu/resolvers/mutations/duplicate.ts
+++ b/back/src/bsvhu/resolvers/mutations/duplicate.ts
@@ -65,7 +65,8 @@ async function getDuplicateData(
user
});
- const { emitter, transporter, destination } = await getBsvhuCompanies(bsvhu);
+ const { emitter, transporter, destination, broker, trader } =
+ await getBsvhuCompanies(bsvhu);
let data: Prisma.BsvhuCreateInput = {
...parsedBsvhu,
@@ -90,7 +91,31 @@ async function getDuplicateData(
transporter?.contactPhone ?? parsedBsvhu.transporterCompanyPhone,
transporterCompanyMail:
transporter?.contactEmail ?? parsedBsvhu.transporterCompanyMail,
- transporterCompanyVatNumber: parsedBsvhu.transporterCompanyVatNumber
+ transporterCompanyVatNumber: parsedBsvhu.transporterCompanyVatNumber,
+ // Broker company info
+ brokerCompanyMail: broker?.contactEmail ?? bsvhu.brokerCompanyMail,
+ brokerCompanyPhone: broker?.contactPhone ?? bsvhu.brokerCompanyPhone,
+ brokerCompanyContact: broker?.contact ?? bsvhu.brokerCompanyContact,
+ // Broker recepisse
+ brokerRecepisseNumber:
+ broker?.brokerReceipt?.receiptNumber ?? bsvhu.brokerRecepisseNumber,
+ brokerRecepisseValidityLimit:
+ broker?.brokerReceipt?.validityLimit ??
+ bsvhu.brokerRecepisseValidityLimit,
+ brokerRecepisseDepartment:
+ broker?.brokerReceipt?.department ?? bsvhu.brokerRecepisseDepartment,
+ // Trader company info
+ traderCompanyMail: trader?.contactEmail ?? bsvhu.traderCompanyMail,
+ traderCompanyPhone: trader?.contactPhone ?? bsvhu.traderCompanyPhone,
+ traderCompanyContact: trader?.contact ?? bsvhu.traderCompanyContact,
+ // Trader recepisse
+ traderRecepisseNumber:
+ trader?.traderReceipt?.receiptNumber ?? bsvhu.traderRecepisseNumber,
+ traderRecepisseValidityLimit:
+ trader?.traderReceipt?.validityLimit ??
+ bsvhu.traderRecepisseValidityLimit,
+ traderRecepisseDepartment:
+ trader?.traderReceipt?.department ?? bsvhu.traderRecepisseDepartment
};
if (intermediaries) {
data = {
@@ -119,14 +144,18 @@ async function getBsvhuCompanies(bsvhu: PrismaBsvhuForParsing) {
bsvhu.emitterCompanySiret,
bsvhu.transporterCompanySiret,
bsvhu.transporterCompanyVatNumber,
- bsvhu.destinationCompanySiret
+ bsvhu.destinationCompanySiret,
+ bsvhu.brokerCompanySiret,
+ bsvhu.traderCompanySiret
].filter(Boolean);
// Batch fetch all companies involved in the BSVHU
const companies = await prisma.company.findMany({
where: { orgId: { in: companiesOrgIds } },
include: {
- transporterReceipt: true
+ transporterReceipt: true,
+ brokerReceipt: true,
+ traderReceipt: true
}
});
@@ -144,5 +173,13 @@ async function getBsvhuCompanies(bsvhu: PrismaBsvhuForParsing) {
company.orgId === bsvhu.transporterCompanyVatNumber
);
- return { emitter, destination, transporter };
+ const broker = companies.find(
+ company => company.orgId === bsvhu.brokerCompanySiret
+ );
+
+ const trader = companies.find(
+ company => company.orgId === bsvhu.traderCompanySiret
+ );
+
+ return { emitter, destination, transporter, broker, trader };
}
diff --git a/back/src/bsvhu/resolvers/queries/__tests__/bsvhu.integration.ts b/back/src/bsvhu/resolvers/queries/__tests__/bsvhu.integration.ts
index 9a4c4fe2d3..b212f84e46 100644
--- a/back/src/bsvhu/resolvers/queries/__tests__/bsvhu.integration.ts
+++ b/back/src/bsvhu/resolvers/queries/__tests__/bsvhu.integration.ts
@@ -40,6 +40,26 @@ query GetBsvhu($id: ID!) {
number
}
}
+ ecoOrganisme {
+ name
+ siret
+ }
+ broker {
+ company {
+ siret
+ }
+ recepisse {
+ number
+ }
+ }
+ trader {
+ company {
+ siret
+ }
+ recepisse {
+ number
+ }
+ }
weight {
value
}
diff --git a/back/src/bsvhu/resolvers/queries/__tests__/bsvhumetadata.integration.ts b/back/src/bsvhu/resolvers/queries/__tests__/bsvhumetadata.integration.ts
index 8d344bb4ea..af75937bd2 100644
--- a/back/src/bsvhu/resolvers/queries/__tests__/bsvhumetadata.integration.ts
+++ b/back/src/bsvhu/resolvers/queries/__tests__/bsvhumetadata.integration.ts
@@ -115,7 +115,7 @@ describe("Query.Bsvhu", () => {
variables: { id: bsd.id }
});
- expect(data.bsvhu.metadata?.fields?.sealed?.length).toBe(21);
+ expect(data.bsvhu.metadata?.fields?.sealed?.length).toBe(24);
});
it("should return TRANSPORTER signed bsvhu sealed fields", async () => {
@@ -139,7 +139,7 @@ describe("Query.Bsvhu", () => {
variables: { id: bsd.id }
});
- expect(data.bsvhu.metadata?.fields?.sealed?.length).toBe(34);
+ expect(data.bsvhu.metadata?.fields?.sealed?.length).toBe(37);
});
it("should return OPERATION signed bsvhu sealed fields", async () => {
@@ -161,6 +161,6 @@ describe("Query.Bsvhu", () => {
variables: { id: bsd.id }
});
- expect(data.bsvhu.metadata?.fields?.sealed?.length).toBe(58);
+ expect(data.bsvhu.metadata?.fields?.sealed?.length).toBe(78);
});
});
diff --git a/back/src/bsvhu/resolvers/queries/__tests__/bsvhus.integration.ts b/back/src/bsvhu/resolvers/queries/__tests__/bsvhus.integration.ts
index 765022524b..5734ffd9a9 100644
--- a/back/src/bsvhu/resolvers/queries/__tests__/bsvhus.integration.ts
+++ b/back/src/bsvhu/resolvers/queries/__tests__/bsvhus.integration.ts
@@ -50,6 +50,22 @@ const GET_BSVHUS = `
number
}
}
+ broker {
+ company {
+ siret
+ }
+ recepisse {
+ number
+ }
+ }
+ trader {
+ company {
+ siret
+ }
+ recepisse {
+ number
+ }
+ }
weight {
value
}
diff --git a/back/src/bsvhu/resolvers/queries/bsvhus.ts b/back/src/bsvhu/resolvers/queries/bsvhus.ts
index debc266ec8..a0c0911db1 100644
--- a/back/src/bsvhu/resolvers/queries/bsvhus.ts
+++ b/back/src/bsvhu/resolvers/queries/bsvhus.ts
@@ -8,6 +8,7 @@ import { getReadonlyBsvhuRepository } from "../../repository";
import { toPrismaWhereInput } from "../../where";
import { Permission, can, getUserRoles } from "../../../permissions";
+import { Prisma } from "@prisma/client";
export default async function bsvhus(
_,
@@ -27,16 +28,32 @@ export default async function bsvhus(
{ transporterCompanySiret: { in: orgIdsWithListPermission } },
{ transporterCompanyVatNumber: { in: orgIdsWithListPermission } },
{ destinationCompanySiret: { in: orgIdsWithListPermission } },
+ { brokerCompanySiret: { in: orgIdsWithListPermission } },
+ { traderCompanySiret: { in: orgIdsWithListPermission } },
{ intermediariesOrgIds: { hasSome: orgIdsWithListPermission } }
]
};
+ const draftMask: Prisma.BsvhuWhereInput = {
+ OR: [
+ {
+ isDraft: false,
+ ...mask
+ },
+ {
+ isDraft: true,
+ canAccessDraftOrgIds: { hasSome: orgIdsWithListPermission },
+ ...mask
+ }
+ ]
+ };
+
const prismaWhere = {
...(whereArgs ? toPrismaWhereInput(whereArgs) : {}),
isDeleted: false
};
- const where = applyMask(prismaWhere, mask);
+ const where = applyMask(prismaWhere, draftMask);
const bsvhuRepository = getReadonlyBsvhuRepository();
const totalCount = await bsvhuRepository.count(where);
diff --git a/back/src/bsvhu/typeDefs/bsvhu.inputs.graphql b/back/src/bsvhu/typeDefs/bsvhu.inputs.graphql
index 172c0b01ff..1457a9f1d6 100644
--- a/back/src/bsvhu/typeDefs/bsvhu.inputs.graphql
+++ b/back/src/bsvhu/typeDefs/bsvhu.inputs.graphql
@@ -16,6 +16,10 @@ input BsvhuWhere {
transporter: BsvhuTransporterWhere
"Filtre sur le champ destination."
destination: BsvhuDestinationWhere
+ "Filtre sur le champ broker."
+ broker: BsvhuBrokerWhere
+ "Filtre sur le champ négociant."
+ trader: BsvhuTraderWhere
"ET logique"
_and: [BsvhuWhere!]
"OU logique"
@@ -59,6 +63,16 @@ input BsvhuDestinationWhere {
operation: BsvhuOperationWhere
}
+"Champs possible pour le filtre sur le courtier."
+input BsvhuBrokerWhere {
+ company: CompanyWhere
+}
+
+"Champs possible pour le filtre sur le négociant."
+input BsvhuTraderWhere {
+ company: CompanyWhere
+}
+
"Champs possibles pour le filtre sur la réception"
input BsvhuReceptionWhere {
date: DateFilter
@@ -89,6 +103,13 @@ input BsvhuInput {
destination: BsvhuDestinationInput
"Détails sur le transporteur"
transporter: BsvhuTransporterInput
+
+ "Courtier"
+ broker: BsvhuBrokerInput
+
+ "Négociant"
+ trader: BsvhuTraderInput
+
"""
Liste d'entreprises intermédiaires. Un intermédiaire est une entreprise qui prend part à la gestion du déchet,
mais pas à la responsabilité de la traçabilité. Il pourra lire ce bordereau, sans étape de signature.
@@ -96,6 +117,9 @@ input BsvhuInput {
Le nombre maximal d'intermédiaires sur un bordereau est de 3.
"""
intermediaries: [CompanyInput!]
+
+ "Eco-organisme"
+ ecoOrganisme: BsvhuEcoOrganismeInput
}
input BsvhuEmitterInput {
@@ -109,6 +133,11 @@ input BsvhuEmitterInput {
company: BsvhuCompanyInput
}
+input BsvhuEcoOrganismeInput {
+ name: String!
+ siret: String!
+}
+
"Extension de CompanyInput ajoutant des champs d'adresse séparés"
input BsvhuCompanyInput {
"""
@@ -232,6 +261,20 @@ input BsvhuTransporterInput {
transport: BsvhuTransportInput
}
+input BsvhuBrokerInput {
+ "Coordonnées de l'entreprise courtier"
+ company: CompanyInput
+ "Récépissé courtier"
+ recepisse: BsvhuRecepisseInput
+}
+
+input BsvhuTraderInput {
+ "Coordonnées de l'entreprise de négoce"
+ company: CompanyInput
+ "Récépissé courtier"
+ recepisse: BsvhuRecepisseInput
+}
+
input BsvhuTransportInput {
"Date de prise en charge"
takenOverAt: DateTime
diff --git a/back/src/bsvhu/typeDefs/bsvhu.objects.graphql b/back/src/bsvhu/typeDefs/bsvhu.objects.graphql
index aef6bc68d6..ad5379216c 100644
--- a/back/src/bsvhu/typeDefs/bsvhu.objects.graphql
+++ b/back/src/bsvhu/typeDefs/bsvhu.objects.graphql
@@ -62,6 +62,15 @@ type Bsvhu {
"""
intermediaries: [FormCompany!]
+ "Eco-organisme"
+ ecoOrganisme: BsvhuEcoOrganisme
+
+ "Courtier"
+ broker: BsvhuBroker
+
+ "Négociant"
+ trader: BsvhuTrader
+
metadata: BsvhuMetadata!
}
@@ -82,6 +91,12 @@ type BsvhuEmission {
signature: Signature
}
+"Information sur l'éco-organisme responsable du BSVHU"
+type BsvhuEcoOrganisme {
+ name: String!
+ siret: String!
+}
+
type BsvhuTransporter {
"Coordonnées de l'entreprise de transport"
company: FormCompany
@@ -100,11 +115,28 @@ type BsvhuTransport {
type BsvhuRecepisse {
"Exemption de récépissé"
isExempted: Boolean
+ "Numéro de récépissé"
number: String
+ "Département"
department: String
+ "Date limite de validité"
validityLimit: DateTime
}
+type BsvhuBroker {
+ "Coordonnées de l'entreprise courtier"
+ company: FormCompany
+ "Récépissé courtier"
+ recepisse: BsvhuRecepisse
+}
+
+type BsvhuTrader {
+ "Coordonnées de l'entreprise de négoce"
+ company: FormCompany
+ "Récépissé négociant"
+ recepisse: BsvhuRecepisse
+}
+
type BsvhuDestination {
"Type de receveur: broyeur ou second centre VHU"
type: BsvhuDestinationType
diff --git a/back/src/bsvhu/utils.ts b/back/src/bsvhu/utils.ts
index f5c23c4e59..b0d23729e1 100644
--- a/back/src/bsvhu/utils.ts
+++ b/back/src/bsvhu/utils.ts
@@ -1,3 +1,6 @@
+import { getUserCompanies } from "../users/database";
+import { BsvhuWithIntermediaries } from "./types";
+
export function getWasteDescription(wasteCode: string | null) {
return wasteCode === "16 01 06"
? "Véhicules hors d'usage ne contenant ni liquides ni autres composants dangereux"
@@ -5,3 +8,37 @@ export function getWasteDescription(wasteCode: string | null) {
? "Véhicules hors d’usage non dépollués par un centre agréé"
: "";
}
+
+export const getCanAccessDraftOrgIds = async (
+ bsvhu: BsvhuWithIntermediaries,
+ userId: string
+): Promise => {
+ const intermediariesOrgIds: string[] = bsvhu.intermediaries
+ ? bsvhu.intermediaries
+ .flatMap(intermediary => [intermediary.siret, intermediary.vatNumber])
+ .filter(Boolean)
+ : [];
+
+ const canAccessDraftOrgIds: string[] = [];
+ if (bsvhu.isDraft) {
+ const userCompanies = await getUserCompanies(userId);
+ const userOrgIds = userCompanies.map(company => company.orgId);
+ const bsvhuOrgIds = [
+ ...intermediariesOrgIds,
+ bsvhu.emitterCompanySiret,
+ ...[
+ bsvhu.transporterCompanySiret,
+ bsvhu.transporterCompanyVatNumber
+ ].filter(Boolean),
+ bsvhu.ecoOrganismeSiret,
+ bsvhu.destinationCompanySiret,
+ bsvhu.traderCompanySiret,
+ bsvhu.brokerCompanySiret
+ ].filter(Boolean);
+ const userOrgIdsInForm = userOrgIds.filter(orgId =>
+ bsvhuOrgIds.includes(orgId)
+ );
+ canAccessDraftOrgIds.push(...userOrgIdsInForm);
+ }
+ return canAccessDraftOrgIds;
+};
diff --git a/back/src/bsvhu/validation/__tests__/validation.integration.ts b/back/src/bsvhu/validation/__tests__/validation.integration.ts
index e8a7ca65b8..60388ff670 100644
--- a/back/src/bsvhu/validation/__tests__/validation.integration.ts
+++ b/back/src/bsvhu/validation/__tests__/validation.integration.ts
@@ -1,7 +1,9 @@
-import { Company, OperationMode } from "@prisma/client";
+import { Company, EcoOrganisme, OperationMode } from "@prisma/client";
import { resetDatabase } from "../../../../integration-tests/helper";
import {
companyFactory,
+ ecoOrganismeFactory,
+ intermediaryReceiptFactory,
transporterReceiptFactory
} from "../../../__tests__/factories";
import { CompanySearchResult } from "../../../companies/types";
@@ -16,6 +18,7 @@ import { BsvhuValidationContext } from "../types";
import { getCurrentSignatureType, prismaToZodBsvhu } from "../helpers";
import { parseBsvhu, parseBsvhuAsync } from "..";
import { ZodError } from "zod";
+import { CompanyRole } from "../../../common/validation/zod/schema";
const searchResult = (companyName: string) => {
return {
@@ -35,13 +38,21 @@ describe("BSVHU validation", () => {
let foreignTransporter: Company;
let transporterCompany: Company;
let intermediaryCompany: Company;
+ let ecoOrganisme: EcoOrganisme;
+ let brokerCompany: Company;
+ let traderCompany: Company;
beforeAll(async () => {
const emitterCompany = await companyFactory({ companyTypes: ["PRODUCER"] });
transporterCompany = await companyFactory({
companyTypes: ["TRANSPORTER"]
});
const destinationCompany = await companyFactory({
- companyTypes: ["WASTE_VEHICLES"]
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR", "DEMOLISSEUR"]
+ });
+ const nextDestinationCompany = await companyFactory({
+ companyTypes: ["WASTE_VEHICLES"],
+ wasteVehiclesTypes: ["BROYEUR"]
});
foreignTransporter = await companyFactory({
companyTypes: ["TRANSPORTER"],
@@ -51,12 +62,26 @@ describe("BSVHU validation", () => {
intermediaryCompany = await companyFactory({
companyTypes: ["INTERMEDIARY"]
});
-
+ ecoOrganisme = await ecoOrganismeFactory({
+ handle: { handleBsvhu: true },
+ createAssociatedCompany: true
+ });
+ brokerCompany = await companyFactory({
+ companyTypes: ["BROKER"]
+ });
+ traderCompany = await companyFactory({
+ companyTypes: ["TRADER"]
+ });
const prismaBsvhu = await bsvhuFactory({
opt: {
emitterCompanySiret: emitterCompany.siret,
transporterCompanySiret: transporterCompany.siret,
destinationCompanySiret: destinationCompany.siret,
+ destinationOperationNextDestinationCompanySiret:
+ nextDestinationCompany.siret,
+ ecoOrganismeSiret: ecoOrganisme.siret,
+ brokerCompanySiret: brokerCompany.siret,
+ traderCompanySiret: traderCompany.siret,
intermediaries: {
create: [toIntermediaryCompany(intermediaryCompany)]
}
@@ -347,9 +372,30 @@ describe("BSVHU validation", () => {
expect((err as ZodError).issues).toEqual([
expect.objectContaining({
message:
- `L'installation de destination avec le SIRET \"${company.siret}\" n'est pas inscrite` +
- " sur Trackdéchets en tant qu'installation de traitement de VHU. Cette installation ne peut donc pas" +
- " être visée sur le bordereau. Veuillez vous rapprocher de l'administrateur de cette installation pour qu'il modifie le profil de l'établissement depuis l'interface Trackdéchets dans Mes établissements"
+ "Cet établissement n'a pas le profil Installation de traitement de VHU."
+ })
+ ]);
+ }
+ });
+
+ test("when next destination is registered with wrong profile", async () => {
+ const company = await companyFactory({ companyTypes: ["PRODUCER"] });
+ const data: ZodBsvhu = {
+ ...bsvhu,
+ destinationOperationNextDestinationCompanySiret: company.siret
+ };
+ expect.assertions(1);
+
+ try {
+ await parseBsvhuAsync(data, {
+ ...context,
+ currentSignatureType: "TRANSPORT"
+ });
+ } catch (err) {
+ expect((err as ZodError).issues).toEqual([
+ expect.objectContaining({
+ message:
+ "Cet établissement n'a pas le profil Installation de traitement de VHU."
})
]);
}
@@ -408,6 +454,30 @@ describe("BSVHU validation", () => {
}
});
+ test("when ecoOrganisme isn't authorized for BSVHU", async () => {
+ const ecoOrg = await ecoOrganismeFactory({
+ handle: { handleBsda: true }
+ });
+ const data: ZodBsvhu = {
+ ...bsvhu,
+ ecoOrganismeSiret: ecoOrg.siret
+ };
+ expect.assertions(1);
+
+ try {
+ await parseBsvhuAsync(data, {
+ ...context,
+ currentSignatureType: "TRANSPORT"
+ });
+ } catch (err) {
+ expect((err as ZodError).issues).toEqual([
+ expect.objectContaining({
+ message: `L'éco-organisme avec le SIRET ${ecoOrg.siret} n'est pas autorisé à apparaitre sur un BSVHU`
+ })
+ ]);
+ }
+ });
+
describe("Emitter transports own waste", () => {
it("allowed if exemption", async () => {
const data: ZodBsvhu = {
@@ -577,7 +647,10 @@ describe("BSVHU validation", () => {
[bsvhu.emitterCompanySiret!]: searchResult("émetteur"),
[bsvhu.transporterCompanySiret!]: searchResult("transporteur"),
[bsvhu.destinationCompanySiret!]: searchResult("destinataire"),
- [intermediaryCompany.siret!]: searchResult("intermédiaire")
+ [intermediaryCompany.siret!]: searchResult("intermédiaire"),
+ [ecoOrganisme.siret!]: searchResult("ecoOrganisme"),
+ [brokerCompany.siret!]: searchResult("broker"),
+ [traderCompany.siret!]: searchResult("trader")
};
(searchCompany as jest.Mock).mockImplementation((clue: string) => {
return Promise.resolve(searchResults[clue]);
@@ -611,6 +684,15 @@ describe("BSVHU validation", () => {
expect(sirenified.intermediaries![0].address).toEqual(
searchResults[intermediaryCompany.siret!].address
);
+ expect(sirenified.ecoOrganismeName).toEqual(
+ searchResults[ecoOrganisme.siret!].name
+ );
+ expect(sirenified.brokerCompanyName).toEqual(
+ searchResults[brokerCompany.siret!].name
+ );
+ expect(sirenified.traderCompanyName).toEqual(
+ searchResults[traderCompany.siret!].name
+ );
});
it("should not overwrite `name` and `address` based on SIRENE data for sealed fields", async () => {
const searchResults = {
@@ -645,10 +727,10 @@ describe("BSVHU validation", () => {
bsvhu.transporterCompanyAddress
);
expect(sirenified.destinationCompanyName).toEqual(
- searchResults[bsvhu.destinationCompanySiret!].name
+ bsvhu.destinationCompanyName
);
expect(sirenified.destinationCompanyAddress).toEqual(
- searchResults[bsvhu.destinationCompanySiret!].address
+ bsvhu.destinationCompanyAddress
);
expect(sirenified.intermediaries![0].name).toEqual(
bsvhu.intermediaries![0].name
@@ -660,7 +742,7 @@ describe("BSVHU validation", () => {
});
describe("BSVHU Recipify Module", () => {
- it("recipify should correctly process input and return completedInput with transporter receipt", async () => {
+ it("recipify should correctly process transporter and return completedInput with transporter receipt", async () => {
const receipt = await transporterReceiptFactory({
company: transporterCompany
});
@@ -694,6 +776,41 @@ describe("BSVHU validation", () => {
})
);
});
+
+ it("recipify should correctly process broker and trader", async () => {
+ const brokerReceipt = await intermediaryReceiptFactory({
+ role: CompanyRole.Broker,
+ company: brokerCompany
+ });
+ const traderReceipt = await intermediaryReceiptFactory({
+ role: CompanyRole.Trader,
+ company: traderCompany
+ });
+ const recipified = await parseBsvhuAsync(
+ {
+ ...bsvhu,
+ brokerRecepisseNumber: null,
+ brokerRecepisseDepartment: null,
+ brokerRecepisseValidityLimit: null,
+ traderRecepisseNumber: null,
+ traderRecepisseDepartment: null,
+ traderRecepisseValidityLimit: null
+ },
+ {
+ ...context
+ }
+ );
+ expect(recipified).toEqual(
+ expect.objectContaining({
+ brokerRecepisseNumber: brokerReceipt.receiptNumber,
+ brokerRecepisseDepartment: brokerReceipt.department,
+ brokerRecepisseValidityLimit: brokerReceipt.validityLimit,
+ traderRecepisseNumber: traderReceipt.receiptNumber,
+ traderRecepisseDepartment: traderReceipt.department,
+ traderRecepisseValidityLimit: traderReceipt.validityLimit
+ })
+ );
+ });
});
describe("getCurrentSignatureType", () => {
diff --git a/back/src/bsvhu/validation/helpers.ts b/back/src/bsvhu/validation/helpers.ts
index c641503d78..761a3357ce 100644
--- a/back/src/bsvhu/validation/helpers.ts
+++ b/back/src/bsvhu/validation/helpers.ts
@@ -113,7 +113,16 @@ export async function getBsvhuUserFunctions(
(bsvhu.transporterCompanySiret != null &&
orgIds.includes(bsvhu.transporterCompanySiret)) ||
(bsvhu.transporterCompanyVatNumber != null &&
- orgIds.includes(bsvhu.transporterCompanyVatNumber))
+ orgIds.includes(bsvhu.transporterCompanyVatNumber)),
+ isEcoOrganisme:
+ bsvhu.ecoOrganismeSiret != null &&
+ orgIds.includes(bsvhu.ecoOrganismeSiret),
+ isBroker:
+ bsvhu.brokerCompanySiret != null &&
+ orgIds.includes(bsvhu.brokerCompanySiret),
+ isTrader:
+ bsvhu.traderCompanySiret != null &&
+ orgIds.includes(bsvhu.traderCompanySiret)
};
}
diff --git a/back/src/bsvhu/validation/recipify.ts b/back/src/bsvhu/validation/recipify.ts
new file mode 100644
index 0000000000..e411c7c106
--- /dev/null
+++ b/back/src/bsvhu/validation/recipify.ts
@@ -0,0 +1,79 @@
+import { getTransporterCompanyOrgId } from "@td/constants";
+import { ParsedZodBsvhu } from "./schema";
+import { CompanyRole } from "../../common/validation/zod/schema";
+import {
+ buildRecipify,
+ RecipifyInputAccessor
+} from "../../common/validation/recipify";
+
+const recipifyBsvhuAccessors = (
+ bsd: ParsedZodBsvhu,
+ // Tranformations should not be run on sealed fields
+ sealedFields: string[]
+): RecipifyInputAccessor[] => [
+ {
+ role: CompanyRole.Transporter,
+ skip: sealedFields.includes("transporterRecepisseNumber"),
+ orgIdGetter: () => {
+ const orgId = getTransporterCompanyOrgId({
+ transporterCompanySiret: bsd.transporterCompanySiret ?? null,
+ transporterCompanyVatNumber: bsd.transporterCompanyVatNumber ?? null
+ });
+ return orgId ?? null;
+ },
+ setter: async (bsvhu: ParsedZodBsvhu, receipt) => {
+ if (bsvhu.transporterRecepisseIsExempted) {
+ bsvhu.transporterRecepisseNumber = null;
+ bsvhu.transporterRecepisseValidityLimit = null;
+ bsvhu.transporterRecepisseDepartment = null;
+ } else {
+ bsvhu.transporterRecepisseNumber = receipt?.receiptNumber ?? null;
+ bsvhu.transporterRecepisseValidityLimit =
+ receipt?.validityLimit ?? null;
+ bsvhu.transporterRecepisseDepartment = receipt?.department ?? null;
+ }
+ }
+ },
+ {
+ role: CompanyRole.Broker,
+ skip: sealedFields.includes("brokerRecepisseNumber"),
+ orgIdGetter: () => {
+ return bsd.brokerCompanySiret ?? null;
+ },
+ setter: async (bsvhu: ParsedZodBsvhu, receipt) => {
+ // don't overwrite user input because there are still those inputs in BSVHU forms
+ if (!bsvhu.brokerRecepisseNumber && receipt?.receiptNumber) {
+ bsvhu.brokerRecepisseNumber = receipt.receiptNumber;
+ }
+ if (!bsvhu.brokerRecepisseValidityLimit && receipt?.validityLimit) {
+ bsvhu.brokerRecepisseValidityLimit = receipt.validityLimit;
+ }
+ if (!bsvhu.brokerRecepisseDepartment && receipt?.department) {
+ bsvhu.brokerRecepisseDepartment = receipt.department;
+ }
+ }
+ },
+ {
+ role: CompanyRole.Trader,
+ skip: sealedFields.includes("traderRecepisseNumber"),
+ orgIdGetter: () => {
+ return bsd.traderCompanySiret ?? null;
+ },
+ setter: async (bsvhu: ParsedZodBsvhu, receipt) => {
+ // don't overwrite user input because there are still those inputs in BSVHU forms
+ if (!bsvhu.traderRecepisseNumber && receipt?.receiptNumber) {
+ bsvhu.traderRecepisseNumber = receipt.receiptNumber;
+ }
+ if (!bsvhu.traderRecepisseValidityLimit && receipt?.validityLimit) {
+ bsvhu.traderRecepisseValidityLimit = receipt.validityLimit;
+ }
+ if (!bsvhu.traderRecepisseDepartment && receipt?.department) {
+ bsvhu.traderRecepisseDepartment = receipt.department;
+ }
+ }
+ }
+];
+
+export const recipifyBsvhu = buildRecipify(
+ recipifyBsvhuAccessors
+);
diff --git a/back/src/bsvhu/validation/refinements.ts b/back/src/bsvhu/validation/refinements.ts
index 25fa4d2cd8..53b21ce7e4 100644
--- a/back/src/bsvhu/validation/refinements.ts
+++ b/back/src/bsvhu/validation/refinements.ts
@@ -10,15 +10,19 @@ import {
import { getSignatureAncestors } from "./helpers";
import { isArray } from "../../common/dataTypes";
import { capitalize } from "../../common/strings";
-import { WasteAcceptationStatus } from "@prisma/client";
+import { BsdType, WasteAcceptationStatus } from "@prisma/client";
import {
destinationOperationModeRefinement,
+ isBrokerRefinement,
isDestinationRefinement,
- isNotDormantRefinement,
+ isEcoOrganismeRefinement,
+ isEmitterNotDormantRefinement,
isRegisteredVatNumberRefinement,
+ isTraderRefinement,
isTransporterRefinement
} from "../../common/validation/zod/refinement";
import { EditionRule } from "./rules";
+import { CompanyRole } from "../../common/validation/zod/schema";
// Date de la MAJ 2024.07.2 introduisant un changement
// des règles de validations sur les poids et volume qui doivent
@@ -33,11 +37,17 @@ export const checkCompanies: Refinement = async (
bsvhu,
zodContext
) => {
- await isNotDormantRefinement(bsvhu.emitterCompanySiret, zodContext);
+ await isEmitterNotDormantRefinement(bsvhu.emitterCompanySiret, zodContext);
await isDestinationRefinement(
bsvhu.destinationCompanySiret,
zodContext,
- "WASTE_VEHICLES"
+ bsvhu.destinationType ?? "WASTE_VEHICLES"
+ );
+ await isDestinationRefinement(
+ bsvhu.destinationOperationNextDestinationCompanySiret,
+ zodContext,
+ "BROYEUR",
+ CompanyRole.DestinationOperationNextDestination
);
await isTransporterRefinement(
{
@@ -51,6 +61,13 @@ export const checkCompanies: Refinement = async (
bsvhu.transporterCompanyVatNumber,
zodContext
);
+ await isEcoOrganismeRefinement(
+ bsvhu.ecoOrganismeSiret,
+ BsdType.BSVHU,
+ zodContext
+ );
+ await isBrokerRefinement(bsvhu.brokerCompanySiret, zodContext);
+ await isTraderRefinement(bsvhu.traderCompanySiret, zodContext);
};
export const checkWeights: Refinement = (
@@ -69,6 +86,7 @@ export const checkWeights: Refinement = (
// On pourra à terme passer de .nonnegative à .positive directement dans le schéma zod.}
addIssue({
code: z.ZodIssueCode.custom,
+ path: ["weight", "value"],
message: "Le poids doit être supérieur à 0"
});
}
@@ -97,6 +115,7 @@ export const checkReceptionWeight: Refinement = (
) {
addIssue({
code: z.ZodIssueCode.custom,
+ path: ["destination", "reception", "weight"],
message:
"destinationReceptionWeight : le poids doit être égal à 0 lorsque le déchet est refusé"
});
@@ -110,6 +129,7 @@ export const checkReceptionWeight: Refinement = (
) {
addIssue({
code: z.ZodIssueCode.custom,
+ path: ["destination", "reception", "weight"],
message:
"destinationReceptionWeight : le poids doit être supérieur à 0 lorsque le déchet est accepté ou accepté partiellement"
});
@@ -125,6 +145,7 @@ export const checkEmitterSituation: Refinement = (
// Le seul cas où l'émetteur peut ne pas avoir de SIRET est si il est en situation irrégulière
addIssue({
code: z.ZodIssueCode.custom,
+ path: ["emitter", "irregularSituation"],
message:
"emitterIrregularSituation : L'émetteur doit obligatoirement avoir un numéro de SIRET si il n'est pas en situation irrégulière"
});
diff --git a/back/src/bsvhu/validation/rules.ts b/back/src/bsvhu/validation/rules.ts
index 1b6e5bafde..ec1f4742c6 100644
--- a/back/src/bsvhu/validation/rules.ts
+++ b/back/src/bsvhu/validation/rules.ts
@@ -197,19 +197,19 @@ export const bsvhuEditionRules: BsvhuEditionRules = {
path: ["destination", "agrementNumber"]
},
destinationCompanyName: {
- sealed: { from: "OPERATION" },
+ sealed: { from: sealedFromEmissionExceptForEmitter },
required: { from: "EMISSION" },
readableFieldName: "La raison sociale du destinataire",
path: ["destination", "company", "name"]
},
destinationCompanySiret: {
- sealed: { from: "OPERATION" },
+ sealed: { from: sealedFromEmissionExceptForEmitter },
required: { from: "EMISSION" },
readableFieldName: "Le N° SIRET du destinataire",
path: ["destination", "company", "siret"]
},
destinationCompanyAddress: {
- sealed: { from: "OPERATION" },
+ sealed: { from: sealedFromEmissionExceptForEmitter },
required: { from: "EMISSION" },
readableFieldName: "L'adresse du destinataire",
path: ["destination", "company", "address"]
@@ -381,6 +381,7 @@ export const bsvhuEditionRules: BsvhuEditionRules = {
},
identificationNumbers: {
sealed: { from: sealedFromEmissionExceptForEmitter },
+ required: { from: "EMISSION" },
readableFieldName: "Les numéros d'identification",
path: ["identification", "numbers"]
},
@@ -511,6 +512,164 @@ export const bsvhuEditionRules: BsvhuEditionRules = {
// from: "TRANSPORT"
// }
},
+ ecoOrganismeName: {
+ readableFieldName: "le nom de l'éco-organisme",
+ sealed: { from: "OPERATION" },
+ path: ["ecoOrganisme", "name"],
+ required: {
+ from: "TRANSPORT",
+ when: bsvhu => !!bsvhu.ecoOrganismeSiret
+ }
+ },
+ ecoOrganismeSiret: {
+ readableFieldName: "le SIRET de l'éco-organisme",
+ sealed: { from: "OPERATION" },
+ path: ["ecoOrganisme", "siret"]
+ },
+ brokerCompanyName: {
+ readableFieldName: "le nom du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.brokerCompanySiret
+ },
+ path: ["broker", "company", "name"]
+ },
+ brokerCompanySiret: {
+ readableFieldName: "le SIRET du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.brokerCompanySiret
+ },
+ path: ["broker", "company", "siret"]
+ },
+ brokerCompanyAddress: {
+ readableFieldName: "l'adresse du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.brokerCompanySiret
+ },
+ path: ["broker", "company", "address"]
+ },
+ brokerCompanyContact: {
+ readableFieldName: "le nom de contact du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.brokerCompanySiret
+ },
+ path: ["broker", "company", "contact"]
+ },
+ brokerCompanyPhone: {
+ readableFieldName: "le téléphone du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.brokerCompanySiret
+ },
+ path: ["broker", "company", "phone"]
+ },
+ brokerCompanyMail: {
+ readableFieldName: "le mail du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.brokerCompanySiret
+ },
+ path: ["broker", "company", "mail"]
+ },
+ brokerRecepisseNumber: {
+ readableFieldName: "le numéro de récépissé du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.brokerCompanySiret
+ },
+ path: ["broker", "recepisse", "number"]
+ },
+ brokerRecepisseDepartment: {
+ readableFieldName: "le département du récépissé du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.brokerCompanySiret
+ },
+ path: ["broker", "recepisse", "department"]
+ },
+ brokerRecepisseValidityLimit: {
+ readableFieldName: "la date de validité du récépissé du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.brokerCompanySiret
+ },
+ path: ["broker", "recepisse", "validityLimit"]
+ },
+ traderCompanyName: {
+ readableFieldName: "le nom du négociant",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.traderCompanySiret
+ },
+ path: ["trader", "company", "name"]
+ },
+ traderCompanySiret: {
+ readableFieldName: "le SIRET du négociant",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.traderCompanySiret
+ },
+ path: ["trader", "company", "siret"]
+ },
+ traderCompanyAddress: {
+ readableFieldName: "l'adresse du négociant",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.traderCompanySiret
+ },
+ path: ["trader", "company", "address"]
+ },
+ traderCompanyContact: {
+ readableFieldName: "le nom de contact du négociant",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.traderCompanySiret
+ },
+ path: ["trader", "company", "contact"]
+ },
+ traderCompanyPhone: {
+ readableFieldName: "le téléphone du négociant",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.traderCompanySiret
+ },
+ path: ["trader", "company", "phone"]
+ },
+ traderCompanyMail: {
+ readableFieldName: "le mail du négociant",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.traderCompanySiret
+ },
+ path: ["trader", "company", "mail"]
+ },
+ traderRecepisseNumber: {
+ readableFieldName: "le numéro de récépissé du négociant",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.traderCompanySiret
+ },
+ path: ["trader", "recepisse", "number"]
+ },
+ traderRecepisseDepartment: {
+ readableFieldName: "le département du récépissé du négociant",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.traderCompanySiret
+ },
+ path: ["trader", "recepisse", "department"]
+ },
+ traderRecepisseValidityLimit: {
+ readableFieldName: "la date de validité du récépissé du courtier",
+ sealed: {
+ from: "OPERATION",
+ when: bsvhu => !!bsvhu.traderCompanySiret
+ },
+ path: ["trader", "recepisse", "validityLimit"]
+ },
intermediaries: {
readableFieldName: "les intermédiaires",
sealed: { from: "TRANSPORT" },
diff --git a/back/src/bsvhu/validation/schema.ts b/back/src/bsvhu/validation/schema.ts
index 018b1fbef8..e0aaafae72 100644
--- a/back/src/bsvhu/validation/schema.ts
+++ b/back/src/bsvhu/validation/schema.ts
@@ -12,8 +12,6 @@ import {
import { BsvhuValidationContext } from "./types";
import { weightSchema } from "../../common/validation/weight";
import { WeightUnits } from "../../common/validation";
-import { sirenifyBsvhu } from "./sirenify";
-import { recipifyBsdTransporter } from "../../common/validation/zod/transformers";
import {
CompanyRole,
foreignVatNumberSchema,
@@ -31,7 +29,7 @@ import {
OperationMode,
WasteAcceptationStatus
} from "@prisma/client";
-import { fillIntermediariesOrgIds } from "./transformers";
+import { fillIntermediariesOrgIds, runTransformers } from "./transformers";
export const ZodWasteCodeEnum = z
.enum(BSVHU_WASTE_CODES, {
@@ -182,6 +180,26 @@ const rawBsvhuSchema = z.object({
.boolean()
.nullish()
.transform(v => Boolean(v)),
+ ecoOrganismeName: z.string().nullish(),
+ ecoOrganismeSiret: siretSchema(CompanyRole.EcoOrganisme).nullish(),
+ brokerCompanyName: z.string().nullish(),
+ brokerCompanySiret: siretSchema(CompanyRole.Broker).nullish(),
+ brokerCompanyAddress: z.string().nullish(),
+ brokerCompanyContact: z.string().nullish(),
+ brokerCompanyPhone: z.string().nullish(),
+ brokerCompanyMail: z.string().nullish(),
+ brokerRecepisseNumber: z.string().nullish(),
+ brokerRecepisseDepartment: z.string().nullish(),
+ brokerRecepisseValidityLimit: z.coerce.date().nullish(),
+ traderCompanyName: z.string().nullish(),
+ traderCompanySiret: siretSchema(CompanyRole.Trader).nullish(),
+ traderCompanyAddress: z.string().nullish(),
+ traderCompanyContact: z.string().nullish(),
+ traderCompanyPhone: z.string().nullish(),
+ traderCompanyMail: z.string().nullish(),
+ traderRecepisseNumber: z.string().nullish(),
+ traderRecepisseDepartment: z.string().nullish(),
+ traderRecepisseValidityLimit: z.coerce.date().nullish(),
intermediaries: z
.array(intermediarySchema)
.nullish()
@@ -230,8 +248,7 @@ export const contextualBsvhuSchema = (context: BsvhuValidationContext) => {
export const contextualBsvhuSchemaAsync = (context: BsvhuValidationContext) => {
return transformedBsvhuSyncSchema
.superRefine(checkCompanies)
- .transform(sirenifyBsvhu(context))
- .transform(recipifyBsdTransporter)
+ .transform((bsvhu: ParsedZodBsvhu) => runTransformers(bsvhu, context))
.superRefine(
// run le check sur les champs requis après les transformations
// au cas où des transformations auto-complète certains champs
diff --git a/back/src/bsvhu/validation/sirenify.ts b/back/src/bsvhu/validation/sirenify.ts
index ddd01cfb7a..32a800981f 100644
--- a/back/src/bsvhu/validation/sirenify.ts
+++ b/back/src/bsvhu/validation/sirenify.ts
@@ -2,9 +2,7 @@ import {
nextBuildSirenify,
NextCompanyInputAccessor
} from "../../companies/sirenify";
-import { getSealedFields } from "./rules";
import { ParsedZodBsvhu } from "./schema";
-import { BsvhuValidationContext, ZodBsvhuTransformer } from "./types";
const sirenifyBsvhuAccessors = (
bsvhu: ParsedZodBsvhu,
@@ -49,6 +47,31 @@ const sirenifyBsvhuAccessors = (
input.transporterCompanyAddress = companyInput.address;
}
},
+ {
+ siret: bsvhu?.ecoOrganismeSiret,
+ skip: sealedFields.includes("ecoOrganismeSiret"),
+ setter: (input, companyInput) => {
+ if (companyInput.name) {
+ input.ecoOrganismeName = companyInput.name;
+ }
+ }
+ },
+ {
+ siret: bsvhu?.brokerCompanySiret,
+ skip: sealedFields.includes("brokerCompanySiret"),
+ setter: (input, companyInput) => {
+ input.brokerCompanyName = companyInput.name;
+ input.brokerCompanyAddress = companyInput.address;
+ }
+ },
+ {
+ siret: bsvhu?.traderCompanySiret,
+ skip: sealedFields.includes("traderCompanySiret"),
+ setter: (input, companyInput) => {
+ input.traderCompanyName = companyInput.name;
+ input.traderCompanyAddress = companyInput.address;
+ }
+ },
...(bsvhu.intermediaries ?? []).map(
(_, idx) =>
({
@@ -67,14 +90,6 @@ const sirenifyBsvhuAccessors = (
)
];
-export const sirenifyBsvhu: (
- context: BsvhuValidationContext
-) => ZodBsvhuTransformer = context => {
- return async bsvhu => {
- const sealedFields = await getSealedFields(bsvhu, context);
- return nextBuildSirenify(sirenifyBsvhuAccessors)(
- bsvhu,
- sealedFields
- );
- };
-};
+export const sirenifyBsvhu = nextBuildSirenify(
+ sirenifyBsvhuAccessors
+);
diff --git a/back/src/bsvhu/validation/transformers.ts b/back/src/bsvhu/validation/transformers.ts
index b957382660..43496b44de 100644
--- a/back/src/bsvhu/validation/transformers.ts
+++ b/back/src/bsvhu/validation/transformers.ts
@@ -1,4 +1,21 @@
-import { ZodBsvhuTransformer } from "./types";
+import { recipifyBsvhu } from "./recipify";
+import { getSealedFields } from "./rules";
+import { ParsedZodBsvhu } from "./schema";
+import { sirenifyBsvhu } from "./sirenify";
+import { BsvhuValidationContext, ZodBsvhuTransformer } from "./types";
+
+export const runTransformers = async (
+ bsvhu: ParsedZodBsvhu,
+ context: BsvhuValidationContext
+): Promise => {
+ const transformers = [sirenifyBsvhu, recipifyBsvhu];
+ const sealedFields = await getSealedFields(bsvhu, context);
+
+ for (const transformer of transformers) {
+ bsvhu = await transformer(bsvhu, sealedFields);
+ }
+ return bsvhu;
+};
export const fillIntermediariesOrgIds: ZodBsvhuTransformer = bsvhu => {
bsvhu.intermediariesOrgIds = bsvhu.intermediaries
diff --git a/back/src/bsvhu/validation/types.ts b/back/src/bsvhu/validation/types.ts
index a2dfb7ecf6..9217951190 100644
--- a/back/src/bsvhu/validation/types.ts
+++ b/back/src/bsvhu/validation/types.ts
@@ -7,6 +7,9 @@ export type BsvhuUserFunctions = {
isEmitter: boolean;
isDestination: boolean;
isTransporter: boolean;
+ isEcoOrganisme: boolean;
+ isBroker: boolean;
+ isTrader: boolean;
};
export type BsvhuValidationContext = {
diff --git a/back/src/bsvhu/where.ts b/back/src/bsvhu/where.ts
index d0dd64f943..810e633c61 100644
--- a/back/src/bsvhu/where.ts
+++ b/back/src/bsvhu/where.ts
@@ -34,7 +34,9 @@ function toPrismaBsvhuWhereInput(where: BsvhuWhere): Prisma.BsvhuWhereInput {
),
destinationOperationSignatureDate: toPrismaDateFilter(
where.destination?.operation?.signature?.date
- )
+ ),
+ brokerCompanySiret: toPrismaStringFilter(where.broker?.company?.siret),
+ traderCompanySiret: toPrismaStringFilter(where.trader?.company?.siret)
});
}
diff --git a/back/src/common/__tests__/elasticHelpers.integration.ts b/back/src/common/__tests__/elasticHelpers.integration.ts
new file mode 100644
index 0000000000..ab5eeead6d
--- /dev/null
+++ b/back/src/common/__tests__/elasticHelpers.integration.ts
@@ -0,0 +1,375 @@
+import {
+ BsdasriStatus,
+ BsdaStatus,
+ BsffStatus,
+ BspaohStatus,
+ BsvhuStatus,
+ Status,
+ WasteAcceptationStatus
+} from "@prisma/client";
+import { resetDatabase } from "../../../integration-tests/helper";
+import {
+ companyFactory,
+ formFactory,
+ userFactory
+} from "../../__tests__/factories";
+import {
+ BsdIndexationConfig,
+ client,
+ index as globalIndex
+} from "../../common/elastic";
+import { reindexAllBsdsInBulk } from "../../bsds/indexation";
+import { cleanUpIsReturnForTab } from "../elasticHelpers";
+import { xDaysAgo } from "../../utils";
+import { bsdaFactory } from "../../bsda/__tests__/factories";
+import { createBsff } from "../../bsffs/__tests__/factories";
+import { bsdasriFactory } from "../../bsdasris/__tests__/factories";
+import { bsvhuFactory } from "../../bsvhu/__tests__/factories.vhu";
+import { bspaohFactory } from "../../bspaoh/__tests__/factories";
+
+describe("elasticHelpers", () => {
+ describe("cleanUpIsReturnForTab", () => {
+ // do not use INDEX_ALIAS_NAME_SEPARATOR
+ const testAlias = "testbsds";
+
+ const testIndex: BsdIndexationConfig = {
+ ...globalIndex,
+ alias: testAlias
+ };
+
+ async function deleteTestIndexes() {
+ await client.indices.delete(
+ { index: `${testAlias}*` },
+ { ignore: [404] }
+ );
+ }
+
+ afterEach(async () => {
+ await resetDatabase();
+ await deleteTestIndexes();
+ });
+
+ const countIsReturnForBSDs = async () => {
+ const countResponse1 = await client.count({
+ index: testIndex.alias,
+ body: {
+ query: {
+ bool: {
+ must: {
+ exists: {
+ field: "isReturnFor"
+ }
+ }
+ }
+ }
+ }
+ });
+
+ return countResponse1.body.count;
+ };
+
+ const changeBSDDestinationReceptionDateInES = async (
+ bsdId: string,
+ date: Date
+ ) => {
+ await client.updateByQuery({
+ index: testIndex.alias,
+ refresh: true,
+ body: {
+ query: {
+ match: {
+ id: bsdId
+ }
+ },
+ script: {
+ source:
+ `ctx._source.destinationReceptionDate = ` +
+ `new SimpleDateFormat('yyyy-MM-dd').parse('${
+ date.toISOString().split("T")[0]
+ }')`
+ }
+ }
+ });
+ };
+
+ const reindex = async () => {
+ await reindexAllBsdsInBulk({ index: testIndex });
+ await client.indices.refresh({
+ index: testIndex.alias
+ });
+ };
+
+ interface CreateOpt {
+ wasteAcceptationStatus: WasteAcceptationStatus;
+ }
+
+ const seedBsdd = async (opt?: CreateOpt) => {
+ const user = await userFactory();
+ const bsdd = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: Status.RECEIVED,
+ receivedAt: new Date(),
+ wasteAcceptationStatus:
+ opt?.wasteAcceptationStatus ??
+ WasteAcceptationStatus.PARTIALLY_REFUSED
+ }
+ });
+
+ return bsdd;
+ };
+
+ const seedBsda = async (opt?: CreateOpt) => {
+ const user = await userFactory();
+ const bsda = await bsdaFactory({
+ userId: user.id,
+ opt: {
+ status: BsdaStatus.PROCESSED,
+ destinationReceptionDate: new Date(),
+ destinationReceptionAcceptationStatus:
+ opt?.wasteAcceptationStatus ??
+ WasteAcceptationStatus.PARTIALLY_REFUSED
+ }
+ });
+
+ return bsda;
+ };
+
+ const seedBsff = async (opt?: CreateOpt) => {
+ const transporter = await companyFactory();
+ const bsff = await createBsff(
+ {},
+ {
+ data: {
+ status: BsffStatus.RECEIVED,
+ destinationReceptionDate: new Date()
+ },
+ packagingData: {
+ acceptationStatus:
+ opt?.wasteAcceptationStatus ??
+ WasteAcceptationStatus.PARTIALLY_REFUSED
+ },
+ transporterData: {
+ transporterCompanySiret: transporter.siret
+ }
+ }
+ );
+
+ return bsff;
+ };
+
+ const seedBsdasri = async (opt?: CreateOpt) => {
+ const transporter = await companyFactory();
+ const bsdasri = await bsdasriFactory({
+ opt: {
+ status: BsdasriStatus.RECEIVED,
+ destinationReceptionDate: new Date(),
+ destinationReceptionAcceptationStatus:
+ opt?.wasteAcceptationStatus ??
+ WasteAcceptationStatus.PARTIALLY_REFUSED,
+ transporterCompanySiret: transporter.siret
+ }
+ });
+
+ return bsdasri;
+ };
+
+ const seedBsvhu = async (opt?: CreateOpt) => {
+ const transporter = await companyFactory();
+ const bsvhu = await bsvhuFactory({
+ opt: {
+ status: BsvhuStatus.PROCESSED,
+ destinationReceptionDate: new Date(),
+ destinationReceptionAcceptationStatus:
+ opt?.wasteAcceptationStatus ??
+ WasteAcceptationStatus.PARTIALLY_REFUSED,
+ transporterCompanySiret: transporter.siret
+ }
+ });
+
+ return bsvhu;
+ };
+
+ const seedBspaoh = async (opt?: CreateOpt) => {
+ const bspaoh = await bspaohFactory({
+ opt: {
+ status: BspaohStatus.RECEIVED,
+ destinationReceptionDate: new Date(),
+ destinationReceptionAcceptationStatus:
+ opt?.wasteAcceptationStatus ??
+ WasteAcceptationStatus.PARTIALLY_REFUSED
+ }
+ });
+
+ return bspaoh;
+ };
+
+ describe.each([
+ ["BSDD", seedBsdd],
+ ["BSDA", seedBsda],
+ ["BSFF", seedBsff],
+ ["BSDASRI", seedBsdasri],
+ ["BSVHU", seedBsvhu],
+ ["BSPAOH", seedBspaoh]
+ ])("%p", (_, bsdCreate) => {
+ it("should not remove fresh BSDs", async () => {
+ // Given
+ await bsdCreate();
+ await reindex();
+
+ // There should be 1 BSD in the isReturnFor tab
+ const count1 = await countIsReturnForBSDs();
+ expect(count1).toEqual(1);
+
+ // Given: now clean up isReturnFor tab
+ await cleanUpIsReturnForTab(testIndex.alias);
+
+ const count2 = await countIsReturnForBSDs();
+ expect(count2).toEqual(1);
+ });
+
+ it("should remove outdated BSDs", async () => {
+ // Given
+ const bsdd = await bsdCreate();
+ await reindex();
+
+ // There should be 1 BSD in the isReturnFor tab
+ const count1 = await countIsReturnForBSDs();
+ expect(count1).toEqual(1);
+
+ // Given
+ // Manually outdate BSD
+ await changeBSDDestinationReceptionDateInES(
+ bsdd.id,
+ xDaysAgo(new Date(), 3)
+ );
+
+ // Clean up tab
+ await cleanUpIsReturnForTab(testIndex.alias);
+
+ // BSD should have disappeared
+ const count2 = await countIsReturnForBSDs();
+ expect(count2).toEqual(0);
+ });
+
+ it("should not touch unconcerned BSDs", async () => {
+ // Given
+ const bsdd = await bsdCreate();
+ // Shall NOT go to isReturnFor tab
+ await bsdCreate({
+ wasteAcceptationStatus: WasteAcceptationStatus.ACCEPTED
+ });
+ await reindex();
+
+ // There should be 1 BSD in the isReturnFor tab
+ const count1 = await countIsReturnForBSDs();
+ expect(count1).toEqual(1);
+
+ // Manually outdate BSD
+ await changeBSDDestinationReceptionDateInES(
+ bsdd.id,
+ xDaysAgo(new Date(), 3)
+ );
+
+ // Given
+ const body = await cleanUpIsReturnForTab(testIndex.alias);
+
+ // BSD should have disappeared
+ expect(body.updated).toEqual(1);
+ });
+ });
+
+ describe("all BSDs mixed together", () => {
+ it("should not remove fresh BSDs", async () => {
+ // Given
+ await seedBsda();
+ await seedBsdasri();
+ await seedBsdd();
+ await seedBsff();
+ await seedBspaoh();
+ await seedBsvhu();
+ await reindex();
+
+ // There should be 6 BSDs in the isReturnFor tab
+ const count1 = await countIsReturnForBSDs();
+ expect(count1).toEqual(6);
+
+ // Given: now clean up isReturnFor tab
+ await cleanUpIsReturnForTab(testIndex.alias);
+
+ const count2 = await countIsReturnForBSDs();
+ expect(count2).toEqual(6);
+ });
+
+ it("should remove outdated BSDs", async () => {
+ // Given
+ const bsda = await seedBsda();
+ const bsdasri = await seedBsdasri();
+ const bsdd = await seedBsdd();
+ const bsff = await seedBsff();
+ const bspaoh = await seedBspaoh();
+ const bsvhu = await seedBsvhu();
+ await reindex();
+
+ // There should be 6 BSDs in the isReturnFor tab
+ const count1 = await countIsReturnForBSDs();
+ expect(count1).toEqual(6);
+
+ // Given
+ // Manually outdate BSD
+ const THREE_DAYS_AGO = xDaysAgo(new Date(), 3);
+ await changeBSDDestinationReceptionDateInES(bsda.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bsdasri.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bsdd.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bsff.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bspaoh.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bsvhu.id, THREE_DAYS_AGO);
+
+ // Clean up tab
+ await cleanUpIsReturnForTab(testIndex.alias);
+
+ // BSDs should have disappeared
+ const count2 = await countIsReturnForBSDs();
+ expect(count2).toEqual(0);
+ });
+
+ it("should not touch unconcerned BSDs", async () => {
+ // Given
+ const bsda = await seedBsda();
+ const bsdasri = await seedBsdasri();
+ const bsdd = await seedBsdd();
+ const bsff = await seedBsff();
+ const bspaoh = await seedBspaoh();
+ const bsvhu = await seedBsvhu();
+ // Shall NOT go to isReturnFor tab
+ const opt = { wasteAcceptationStatus: WasteAcceptationStatus.ACCEPTED };
+ await seedBsda(opt);
+ await seedBsdasri(opt);
+ await seedBsdd(opt);
+ await seedBsff(opt);
+ await seedBspaoh(opt);
+ await seedBsvhu(opt);
+ await reindex();
+
+ // There should be 1 BSD in the isReturnFor tab
+ const count1 = await countIsReturnForBSDs();
+ expect(count1).toEqual(6);
+
+ // Manually outdate BSD
+ const THREE_DAYS_AGO = xDaysAgo(new Date(), 3);
+ await changeBSDDestinationReceptionDateInES(bsda.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bsdasri.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bsdd.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bsff.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bspaoh.id, THREE_DAYS_AGO);
+ await changeBSDDestinationReceptionDateInES(bsvhu.id, THREE_DAYS_AGO);
+
+ // Given
+ const body = await cleanUpIsReturnForTab(testIndex.alias);
+
+ // BSDs should have disappeared
+ expect(body.updated).toEqual(6);
+ });
+ });
+ });
+});
diff --git a/back/src/common/elastic.ts b/back/src/common/elastic.ts
index bbd93fc10e..444189d681 100644
--- a/back/src/common/elastic.ts
+++ b/back/src/common/elastic.ts
@@ -97,6 +97,7 @@ export interface BsdElastic {
isArchivedFor: string[];
isToCollectFor: string[];
isCollectedFor: string[];
+ isReturnFor: string[];
sirets: string[];
isIncomingWasteFor: string[];
@@ -286,6 +287,7 @@ const properties: Record> = {
isArchivedFor: stringField,
isToCollectFor: stringField,
isCollectedFor: stringField,
+ isReturnFor: stringField,
sirets: stringField,
isIncomingWasteFor: stringField,
diff --git a/back/src/common/elasticHelpers.ts b/back/src/common/elasticHelpers.ts
index 0c8395c857..eeb2de54a7 100644
--- a/back/src/common/elasticHelpers.ts
+++ b/back/src/common/elasticHelpers.ts
@@ -1,5 +1,9 @@
-import { BsdElastic } from "./elastic";
+import { ApiResponse } from "@elastic/elasticsearch";
+import { UpdateByQueryResponse } from "@elastic/elasticsearch/api/types";
+import { xDaysAgo } from "../utils";
+import { BsdElastic, client, index } from "./elastic";
import { RevisionRequestStatus } from "@prisma/client";
+import { logger } from "@td/logger";
type RevisionRequest = {
status: RevisionRequestStatus;
@@ -61,3 +65,45 @@ export function getRevisionOrgIds(
isRevisedFor: isRevisedFor.filter(orgId => !isInRevisionFor.includes(orgId))
};
}
+/**
+ * The "isReturnFor" tab contains BSDs with characteristics that make them valuable for
+ * transporters, but only for so long. That's why we need to regularly clean this tab up
+ */
+export const cleanUpIsReturnForTab = async (alias = index.alias) => {
+ const twoDaysAgo = xDaysAgo(new Date(), 2);
+
+ const { body }: ApiResponse =
+ await client.updateByQuery({
+ index: alias,
+ refresh: true,
+ body: {
+ query: {
+ bool: {
+ filter: [
+ {
+ exists: {
+ field: "isReturnFor"
+ }
+ },
+ {
+ range: {
+ destinationReceptionDate: {
+ lt: twoDaysAgo.toISOString()
+ }
+ }
+ }
+ ]
+ }
+ },
+ script: {
+ source: "ctx._source.isReturnFor = []"
+ }
+ }
+ });
+
+ logger.info(
+ `[cleanUpIsReturnForTab] Update ended! ${body.updated} bsds updated in ${body.took}ms!`
+ );
+
+ return body;
+};
diff --git a/back/src/common/helpers.ts b/back/src/common/helpers.ts
index 8823aff37f..10981449f6 100644
--- a/back/src/common/helpers.ts
+++ b/back/src/common/helpers.ts
@@ -19,3 +19,20 @@ export const dateToXMonthAtHHMM = (date: Date = new Date()): string => {
export const isDefined = (obj: any) => {
return obj !== null && obj !== undefined;
};
+
+/**
+ * This one does not consider empty strings "" as 'defined'
+ */
+export const isDefinedStrict = (val: any): boolean => {
+ if (val === "") return false;
+
+ return isDefined(val);
+};
+
+/*
+ * Tests if a list of objects are ALL defined. 0 will be considered as defined
+ */
+export const areDefined = (...obj: any[]) => {
+ if (obj.some(o => !isDefined(o))) return false;
+ return true;
+};
diff --git a/back/src/common/pdf/components/Recepisse.tsx b/back/src/common/pdf/components/Recepisse.tsx
index 161ad271e5..14f9f1f5fa 100644
--- a/back/src/common/pdf/components/Recepisse.tsx
+++ b/back/src/common/pdf/components/Recepisse.tsx
@@ -1,20 +1,23 @@
import * as React from "react";
import { formatDate } from "../../../common/pdf";
-import {
- BsdaRecepisse,
- BsffTransporterRecepisse
-} from "../../../generated/graphql/types";
type Props = {
- recepisse: BsdaRecepisse | BsffTransporterRecepisse | null | undefined;
+ recepisse:
+ | {
+ number?: string | number | null;
+ department?: string | null;
+ validityLimit?: Date | null;
+ }
+ | null
+ | undefined;
};
export function Recepisse({ recepisse }: Readonly) {
return (
- Récépissé n° : {recepisse?.number}
+ Récépissé n° : {recepisse?.number ?? "-"}
- Département : {recepisse?.department}
+ Département : {recepisse?.department ?? "-"}
Limite de validité : {formatDate(recepisse?.validityLimit)}
diff --git a/back/src/common/validation/recipify.ts b/back/src/common/validation/recipify.ts
index d3d94cf45f..4543412dbd 100644
--- a/back/src/common/validation/recipify.ts
+++ b/back/src/common/validation/recipify.ts
@@ -1,53 +1,77 @@
-import {
- BsdaInput,
- BsdasriInput,
- BsdasriRecepisseInput,
- BsvhuInput,
- BsvhuRecepisseInput
-} from "../../generated/graphql/types";
+import { prisma } from "@td/prisma";
+import { CompanyRole } from "./zod/schema";
-import { recipifyGeneric } from "../../companies/recipify";
+type Receipt = {
+ receiptNumber: string;
+ validityLimit: Date;
+ department: string;
+};
-/**
- * Generic setter for Bsvhu and Bsdasri
- * @param input auto-completed Input
- * @param recepisseInput Original input
- * @returns
- */
-export const autocompletedRecepisse = (
- input: BsvhuInput | BsdasriInput,
- recepisseInput: BsvhuRecepisseInput | BsdasriRecepisseInput
-) => ({
- ...input.transporter?.recepisse,
- ...recepisseInput
-});
+export type RecipifyInputAccessor = {
+ role: CompanyRole.Transporter | CompanyRole.Broker | CompanyRole.Trader;
+ skip: boolean;
+ orgIdGetter: () => string | null;
+ setter: (input: T, receipt: Receipt | null) => Promise;
+};
-/**
- * Generic getter for Bsda, Bsvhu and Bsdasri
- * Null when exempted or if transporter company has not changed
- * @param input auto-completed Input
- * @returns
- */
-export const genericGetter = (input: BsvhuInput | BsdasriInput) => () =>
- input?.transporter?.recepisse?.isExempted !== true
- ? input?.transporter?.company
- : null;
-
-const accessors = (input: BsdaInput) => [
- {
- getter: genericGetter(input),
- setter: (
- input: BsvhuInput | BsdasriInput,
- recepisseInput: BsvhuRecepisseInput | BsdasriRecepisseInput
- ) => ({
- ...input,
- transporter: {
- company: input.transporter?.company,
- transport: input.transporter?.transport,
- recepisse: autocompletedRecepisse(input, recepisseInput)
+export const buildRecipify = (
+ companyInputAccessors: (
+ input: T,
+ sealedFields: string[]
+ ) => RecipifyInputAccessor[]
+): ((bsd: T, sealedFields: string[]) => Promise) => {
+ return async (bsd, sealedFields) => {
+ const accessors = companyInputAccessors(bsd, sealedFields);
+ const recipifiedBsd = { ...bsd };
+ for (const { role, skip, setter, orgIdGetter } of accessors) {
+ if (skip) {
+ continue;
+ }
+ let receipt: Receipt | null = null;
+ const orgId = orgIdGetter();
+ if (!orgId) {
+ continue;
+ }
+ if (role === CompanyRole.Transporter) {
+ try {
+ receipt = await prisma.company
+ .findUnique({
+ where: {
+ orgId
+ }
+ })
+ .transporterReceipt();
+ } catch (error) {
+ // do nothing
+ }
+ } else if (role === CompanyRole.Broker) {
+ try {
+ receipt = await prisma.company
+ .findUnique({
+ where: {
+ orgId
+ }
+ })
+ .brokerReceipt();
+ } catch (error) {
+ // do nothing
+ }
+ } else if (role === CompanyRole.Trader) {
+ try {
+ receipt = await prisma.company
+ .findUnique({
+ where: {
+ orgId
+ }
+ })
+ .traderReceipt();
+ } catch (error) {
+ // do nothing
+ }
}
- })
- }
-];
+ setter(recipifiedBsd, receipt);
+ }
-export const recipify = recipifyGeneric(accessors);
+ return recipifiedBsd;
+ };
+};
diff --git a/back/src/common/validation/zod/refinement.ts b/back/src/common/validation/zod/refinement.ts
index 0fc8e9ca14..5623a31d0d 100644
--- a/back/src/common/validation/zod/refinement.ts
+++ b/back/src/common/validation/zod/refinement.ts
@@ -1,15 +1,19 @@
import { RefinementCtx, z } from "zod";
import {
+ isBroker,
+ isBroyeur,
isCollector,
+ isDemolisseur,
+ isTrader,
isTransporter,
isWasteCenter,
isWasteProcessor,
isWasteVehicles
} from "../../../companies/validation";
import { prisma } from "@td/prisma";
-import { Company, CompanyVerificationStatus } from "@prisma/client";
+import { BsdType, Company, CompanyVerificationStatus } from "@prisma/client";
import { getOperationModesFromOperationCode } from "../../operationModes";
-import { CompanyRole } from "./schema";
+import { CompanyRole, pathFromCompanyRole } from "./schema";
const { VERIFY_COMPANY } = process.env;
@@ -34,6 +38,7 @@ export async function isTransporterRefinement(
if (company && !isTransporter(company)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.Transporter),
message:
`Le transporteur saisi sur le bordereau (SIRET: ${siret}) n'est pas inscrit sur Trackdéchets` +
` en tant qu'entreprise de transport. Cette entreprise ne peut donc pas être visée sur le bordereau.` +
@@ -55,6 +60,7 @@ export async function refineSiretAndGetCompany(
if (company === null) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(companyRole),
message: `${
companyRole ? `${companyRole} : ` : ""
}L'établissement avec le SIRET ${siret} n'est pas inscrit sur Trackdéchets`
@@ -64,12 +70,34 @@ export async function refineSiretAndGetCompany(
if (company?.isDormantSince) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(companyRole),
message: `L'établissement avec le SIRET ${siret} est en sommeil sur Trackdéchets, il n'est pas possible de le mentionner sur un bordereau`
});
}
return company;
}
+
+export async function refineAndGetEcoOrganisme(
+ siret: string | null | undefined,
+ ctx: RefinementCtx
+) {
+ if (!siret) return null;
+ const ecoOrganisme = await prisma.ecoOrganisme.findUnique({
+ where: { siret }
+ });
+
+ if (ecoOrganisme === null) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: ["ecoOrganisme", "siret"],
+ message: `L'éco-organisme avec le SIRET ${siret} n'est pas référencé sur Trackdéchets`
+ });
+ }
+
+ return ecoOrganisme;
+}
+
export const isRegisteredVatNumberRefinement = async (
vatNumber: string | null | undefined,
ctx: RefinementCtx
@@ -81,12 +109,14 @@ export const isRegisteredVatNumberRefinement = async (
if (company === null) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.Transporter),
message: `Le transporteur avec le n°de TVA ${vatNumber} n'est pas inscrit sur Trackdéchets`
});
}
if (!isTransporter(company)) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.Transporter),
message:
`Le transporteur saisi sur le bordereau (numéro de TVA: ${vatNumber}) n'est pas inscrit sur Trackdéchets` +
` en tant qu'entreprise de transport. Cette entreprise ne peut donc pas être visée sur le bordereau.` +
@@ -98,60 +128,79 @@ export const isRegisteredVatNumberRefinement = async (
export async function isDestinationRefinement(
siret: string | null | undefined,
ctx: RefinementCtx,
- role: "DESTINATION" | "WASTE_VEHICLES" = "DESTINATION",
+ role:
+ | "DESTINATION"
+ | "WASTE_VEHICLES"
+ | "BROYEUR"
+ | "DEMOLISSEUR" = "DESTINATION",
+ bsdCompanyRole: CompanyRole = CompanyRole.Destination,
isExemptedFromVerification?: (destination: Company | null) => boolean
) {
- const company = await refineSiretAndGetCompany(
- siret,
- ctx,
- CompanyRole.Destination
- );
+ const company = await refineSiretAndGetCompany(siret, ctx, bsdCompanyRole);
+ if (company) {
+ if (
+ role === "WASTE_VEHICLES" ||
+ role === "BROYEUR" ||
+ role === "DEMOLISSEUR"
+ ) {
+ if (!isWasteVehicles(company)) {
+ return ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(bsdCompanyRole),
+ message: `Cet établissement n'a pas le profil Installation de traitement de VHU.`
+ });
+ }
+ if (role === "BROYEUR" && !isBroyeur(company)) {
+ return ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(bsdCompanyRole),
+ message: `Cet établissement n'a pas le sous-profil Broyeur.`
+ });
+ }
+ if (role === "DEMOLISSEUR" && !isDemolisseur(company)) {
+ return ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(bsdCompanyRole),
+ message: `Cet établissement n'a pas le sous-profil Casse automobile / démolisseur.`
+ });
+ }
+ } else if (
+ !isCollector(company) &&
+ !isWasteProcessor(company) &&
+ !isWasteCenter(company) &&
+ !isWasteVehicles(company)
+ ) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(bsdCompanyRole),
+ message:
+ `L'installation de destination ou d’entreposage ou de reconditionnement avec le SIRET "${siret}" n'est pas inscrite` +
+ ` sur Trackdéchets en tant qu'installation de traitement ou de tri transit regroupement. Cette installation ne peut` +
+ ` donc pas être visée sur le bordereau. Veuillez vous rapprocher de l'administrateur de cette installation pour qu'il` +
+ ` modifie le profil de l'établissement depuis l'interface Trackdéchets dans Mes établissements`
+ });
+ }
- if (company && role === "WASTE_VEHICLES" && !isWasteVehicles(company)) {
- return ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message:
- `L'installation de destination avec le SIRET "${siret}" n'est pas inscrite` +
- ` sur Trackdéchets en tant qu'installation de traitement de VHU. Cette installation ne peut` +
- ` donc pas être visée sur le bordereau. Veuillez vous rapprocher de l'administrateur de cette installation pour qu'il` +
- ` modifie le profil de l'établissement depuis l'interface Trackdéchets dans Mes établissements`
- });
- } else if (
- company &&
- !isCollector(company) &&
- !isWasteProcessor(company) &&
- !isWasteCenter(company) &&
- !isWasteVehicles(company)
- ) {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message:
- `L'installation de destination ou d’entreposage ou de reconditionnement avec le SIRET "${siret}" n'est pas inscrite` +
- ` sur Trackdéchets en tant qu'installation de traitement ou de tri transit regroupement. Cette installation ne peut` +
- ` donc pas être visée sur le bordereau. Veuillez vous rapprocher de l'administrateur de cette installation pour qu'il` +
- ` modifie le profil de l'établissement depuis l'interface Trackdéchets dans Mes établissements`
- });
- }
+ if (
+ VERIFY_COMPANY === "true" &&
+ company.verificationStatus !== CompanyVerificationStatus.VERIFIED
+ ) {
+ if (isExemptedFromVerification && isExemptedFromVerification(company)) {
+ return true;
+ }
- if (
- company &&
- VERIFY_COMPANY === "true" &&
- company.verificationStatus !== CompanyVerificationStatus.VERIFIED
- ) {
- if (isExemptedFromVerification && isExemptedFromVerification(company)) {
- return true;
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(bsdCompanyRole),
+ message:
+ `Le compte de l'installation de destination ou d’entreposage ou de reconditionnement prévue` +
+ ` avec le SIRET ${siret} n'a pas encore été vérifié. Cette installation ne peut pas être visée sur le bordereau.`
+ });
}
-
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message:
- `Le compte de l'installation de destination ou d’entreposage ou de reconditionnement prévue` +
- ` avec le SIRET ${siret} n'a pas encore été vérifié. Cette installation ne peut pas être visée sur le bordereau.`
- });
}
}
-export async function isNotDormantRefinement(
+export async function isEmitterNotDormantRefinement(
siret: string | null | undefined,
ctx: RefinementCtx
) {
@@ -163,6 +212,7 @@ export async function isNotDormantRefinement(
if (company?.isDormantSince) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.Emitter),
message: `L'établissement avec le SIRET ${siret} est en sommeil sur Trackdéchets, il n'est pas possible de le mentionner sur un bordereau`
});
}
@@ -179,6 +229,7 @@ export function destinationOperationModeRefinement(
if (modes.length && !destinationOperationMode) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.Destination),
message: "Vous devez préciser un mode de traitement"
});
} else if (
@@ -189,9 +240,72 @@ export function destinationOperationModeRefinement(
) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.Destination),
message:
"Le mode de traitement n'est pas compatible avec l'opération de traitement choisie"
});
}
}
}
+
+export async function isEcoOrganismeRefinement(
+ siret: string | null | undefined,
+ bsdType: BsdType,
+ ctx: RefinementCtx
+) {
+ const ecoOrganisme = await refineAndGetEcoOrganisme(siret, ctx);
+
+ if (ecoOrganisme) {
+ if (bsdType === BsdType.BSDA && !ecoOrganisme.handleBsda) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.EcoOrganisme),
+ message: `L'éco-organisme avec le SIRET ${siret} n'est pas autorisé à apparaitre sur un BSDA`
+ });
+ } else if (bsdType === BsdType.BSVHU && !ecoOrganisme.handleBsvhu) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.EcoOrganisme),
+ message: `L'éco-organisme avec le SIRET ${siret} n'est pas autorisé à apparaitre sur un BSVHU`
+ });
+ }
+ }
+}
+
+export async function isBrokerRefinement(
+ siret: string | null | undefined,
+ ctx: RefinementCtx
+) {
+ const company = await refineSiretAndGetCompany(
+ siret,
+ ctx,
+ CompanyRole.Broker
+ );
+
+ if (company && !isBroker(company)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.Broker),
+ message: `Cet établissement n'a pas le profil Courtier.`
+ });
+ }
+}
+
+export async function isTraderRefinement(
+ siret: string | null | undefined,
+ ctx: RefinementCtx
+) {
+ const company = await refineSiretAndGetCompany(
+ siret,
+ ctx,
+ CompanyRole.Trader
+ );
+
+ if (company && !isTrader(company)) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: pathFromCompanyRole(CompanyRole.Trader),
+ message: `Cet établissement n'a pas le profil Négociant.`
+ });
+ }
+}
diff --git a/back/src/common/validation/zod/schema.ts b/back/src/common/validation/zod/schema.ts
index 20d9413e70..2663289fa1 100644
--- a/back/src/common/validation/zod/schema.ts
+++ b/back/src/common/validation/zod/schema.ts
@@ -8,11 +8,46 @@ export enum CompanyRole {
Destination = "Destination",
EcoOrganisme = "Éco-organisme",
Broker = "Courtier",
+ Trader = "Trader",
Worker = "Entreprise de travaux",
Intermediary = "Intermédiaire",
- NextDestination = "Éxutoire"
+ NextDestination = "Éxutoire",
+ DestinationOperationNextDestination = "Éxutoire final"
}
+export const pathFromCompanyRole = (companyRole?: CompanyRole): string[] => {
+ switch (companyRole) {
+ case CompanyRole.Emitter:
+ return ["emitter", "company", "siret"];
+ case CompanyRole.Transporter:
+ return ["transporter", "company", "siret"];
+ case CompanyRole.Destination:
+ return ["destination", "company", "siret"];
+ case CompanyRole.EcoOrganisme:
+ return ["ecoOrganisme", "siret"];
+ case CompanyRole.Broker:
+ return ["broker", "company", "siret"];
+ case CompanyRole.Trader:
+ return ["trader", "company", "siret"];
+ case CompanyRole.Worker:
+ return ["worker", "company", "siret"];
+ case CompanyRole.Intermediary:
+ return ["intermediaries", "company", "siret"];
+ case CompanyRole.NextDestination:
+ return ["nextDestination", "company", "siret"];
+ case CompanyRole.DestinationOperationNextDestination:
+ return [
+ "destination",
+ "operation",
+ "nextDestination",
+ "company",
+ "siret"
+ ];
+ default:
+ return [];
+ }
+};
+
export const siretSchema = (expectedCompanyRole?: CompanyRole) =>
z
.string({
@@ -28,6 +63,7 @@ export const siretSchema = (expectedCompanyRole?: CompanyRole) =>
return isSiret(value);
},
val => ({
+ path: pathFromCompanyRole(expectedCompanyRole),
message: `${
expectedCompanyRole ? `${expectedCompanyRole} : ` : ""
}${val} n'est pas un numéro de SIRET valide`
@@ -43,10 +79,18 @@ export const vatNumberSchema = z.string().refine(
val => ({ message: `${val} n'est pas un numéro de TVA valide` })
);
export const foreignVatNumberSchema = (expectedCompanyRole?: CompanyRole) =>
- vatNumberSchema.refine(value => {
- if (!value) return true;
- return isForeignVat(value);
- }, `${expectedCompanyRole ? `${expectedCompanyRole} : ` : ""}Impossible d'utiliser le numéro de TVA pour un établissement français, veuillez renseigner son SIRET uniquement`);
+ vatNumberSchema.refine(
+ value => {
+ if (!value) return true;
+ return isForeignVat(value);
+ },
+ {
+ path: pathFromCompanyRole(expectedCompanyRole),
+ message: `${
+ expectedCompanyRole ? `${expectedCompanyRole} : ` : ""
+ }Impossible d'utiliser le numéro de TVA pour un établissement français, veuillez renseigner son SIRET uniquement`
+ }
+ );
export const rawTransporterSchema = z.object({
id: z.string().nullish(),
diff --git a/back/src/companies/converters.ts b/back/src/companies/converters.ts
new file mode 100644
index 0000000000..881c7e18d1
--- /dev/null
+++ b/back/src/companies/converters.ts
@@ -0,0 +1,18 @@
+import { Company } from "@prisma/client";
+import { CompanyPrivate } from "../generated/graphql/types";
+import { libelleFromCodeNaf } from "./sirene/utils";
+
+export function toGqlCompanyPrivate(company: Company): CompanyPrivate {
+ return {
+ ...company,
+ ecoOrganismeAgreements:
+ company.ecoOrganismeAgreements?.map(a => new URL(a)) ?? [],
+ naf: company.codeNaf,
+ libelleNaf: company.codeNaf ? libelleFromCodeNaf(company.codeNaf) : "",
+ // les champs ci-dessous sont calculés dans le resolver CompanyPrivate
+ signatureAutomations: [],
+ receivedSignatureAutomations: [],
+ userPermissions: [],
+ userNotifications: []
+ };
+}
diff --git a/back/src/companies/database.ts b/back/src/companies/database.ts
index b466bde04c..7d12b93e46 100644
--- a/back/src/companies/database.ts
+++ b/back/src/companies/database.ts
@@ -8,7 +8,9 @@ import {
Prisma,
Company,
CompanyAssociation,
- UserAccountHash
+ UserAccountHash,
+ UserNotification,
+ UserRole
} from "@prisma/client";
import {
CompanyNotFound,
@@ -31,6 +33,7 @@ import {
searchVatFrOnlyOrNotFoundFailFast
} from "./search";
import { SireneSearchResult } from "./sirene/types";
+import { ALL_NOTIFICATIONS } from "../users/notifications";
/**
* Retrieves a company by any unique identifier or throw a CompanyNotFound error
@@ -185,22 +188,23 @@ export const userNameDisplay = (
/**
* map a user's company association to a CompanyMember
- * @param companyAssociations
+ * @param companyAssociation
* @param company
* @param isTDAdmin
*/
export const userAssociationToCompanyMember = (
- companyAssociations: CompanyAssociation & { user: User },
+ companyAssociation: CompanyAssociation & { user: User },
orgId: string,
requestingUserId?: string,
isTDAdmin = false
-) => {
+): CompanyMember => {
return {
- ...companyAssociations.user,
+ ...companyAssociation.user,
orgId: orgId,
- name: userNameDisplay(companyAssociations, requestingUserId, isTDAdmin),
- role: companyAssociations.role,
- isPendingInvitation: false
+ name: userNameDisplay(companyAssociation, requestingUserId, isTDAdmin),
+ role: companyAssociation.role,
+ isPendingInvitation: false,
+ notifications: companyAssociation.notifications
};
};
@@ -213,7 +217,7 @@ export const userAssociationToCompanyMember = (
export const userAccountHashToCompanyMember = (
userAccountHash: UserAccountHash,
orgId: string
-) => {
+): CompanyMember => {
return {
id: userAccountHash.id,
orgId: orgId,
@@ -221,7 +225,9 @@ export const userAccountHashToCompanyMember = (
email: userAccountHash.email,
role: userAccountHash.role,
isActive: false,
- isPendingInvitation: true
+ isPendingInvitation: true,
+ notifications:
+ userAccountHash.role === UserRole.ADMIN ? ALL_NOTIFICATIONS : []
};
};
@@ -308,31 +314,37 @@ export async function getActiveAdminsByCompanyIds(
}
/**
- * Get all the companies and admins from companies, by companyOrgIds
+ * Get all the companies and subscribers to notification from companies, by companyOrgIds
* Will return an object like:
* {
- * [ordId]: { ...company, admins: user[] }
+ * [ordId]: { ...company, subscribers: user[] }
* }
*/
-export const getCompaniesAndActiveAdminsByCompanyOrgIds = async (
- orgIds: string[]
-): Promise> => {
+export const getCompaniesAndSubscribersByCompanyOrgIds = async (
+ orgIds: string[],
+ notification: UserNotification
+): Promise> => {
const companies = await prisma.company.findMany({
where: { orgId: { in: orgIds } },
include: {
companyAssociations: {
- where: { role: "ADMIN", user: { isActive: true } },
+ where: {
+ notifications: { has: notification },
+ user: {
+ isActive: true
+ }
+ },
include: { user: true, company: true }
}
}
});
- return companies.reduce>(
+ return companies.reduce>(
(companiesAndAdminsByOrgId, { companyAssociations, ...company }) => ({
...companiesAndAdminsByOrgId,
[company.orgId]: {
...company,
- admins: companyAssociations.map(({ user }) => user)
+ subscribers: companyAssociations.map(({ user }) => user)
}
}),
{}
@@ -391,24 +403,6 @@ export async function getWorkerCertificationOrNotFound({
return agrement;
}
-export function convertUrls>(
- company: T
-): T & {
- ecoOrganismeAgreements: URL[];
- signatureAutomations: [];
- receivedSignatureAutomations: [];
- userPermissions: [];
-} {
- return {
- ...company,
- ecoOrganismeAgreements:
- company.ecoOrganismeAgreements?.map(a => new URL(a)) ?? [],
- signatureAutomations: [],
- receivedSignatureAutomations: [],
- userPermissions: []
- };
-}
-
export async function updateFavorites(orgIds: string[]) {
for (const favoriteType of allFavoriteTypes) {
await favoritesCompanyQueue.addBulk(
diff --git a/back/src/companies/dataloaders.ts b/back/src/companies/dataloaders.ts
index f67a238862..cd9045b191 100644
--- a/back/src/companies/dataloaders.ts
+++ b/back/src/companies/dataloaders.ts
@@ -5,7 +5,7 @@ import { UserRole } from "@prisma/client";
export function createCompanyDataLoaders() {
return {
installations: new DataLoader((sirets: string[]) =>
- genInstallations(sirets)
+ getInstallations(sirets)
),
companiesAdmin: new DataLoader((companyIds: string[]) =>
getCompaniesAdmin(companyIds)
@@ -13,7 +13,7 @@ export function createCompanyDataLoaders() {
};
}
-async function genInstallations(sirets: string[]) {
+async function getInstallations(sirets: string[]) {
const installations = await prisma.installation.findMany({
where: {
OR: [
diff --git a/back/src/companies/resolvers/CompanyPrivate.ts b/back/src/companies/resolvers/CompanyPrivate.ts
index 3799a394c9..7afd378e21 100644
--- a/back/src/companies/resolvers/CompanyPrivate.ts
+++ b/back/src/companies/resolvers/CompanyPrivate.ts
@@ -27,6 +27,20 @@ const companyPrivateResolvers: CompanyPrivateResolvers = {
parent.userRole ?? (await getUserRole(context.user!.id, parent.orgId));
return role ? grants[role].map(toGraphQLPermission) : [];
},
+ userNotifications: async (parent, _, context) => {
+ if (!context.user) {
+ return [];
+ }
+ const companyAssociations = await prisma.company
+ .findUnique({ where: { id: parent.id } })
+ .companyAssociations({ where: { userId: context.user.id } });
+
+ if (companyAssociations?.length) {
+ return companyAssociations[0].notifications;
+ }
+
+ return [];
+ },
transporterReceipt: parent => {
return prisma.company
.findUnique({ where: { id: parent.id } })
diff --git a/back/src/companies/resolvers/mutations/__tests__/createCompany.integration.ts b/back/src/companies/resolvers/mutations/__tests__/createCompany.integration.ts
index 999cdfa5ef..dab07eeb33 100644
--- a/back/src/companies/resolvers/mutations/__tests__/createCompany.integration.ts
+++ b/back/src/companies/resolvers/mutations/__tests__/createCompany.integration.ts
@@ -31,6 +31,7 @@ import {
import { searchCompany } from "../../../search";
import { sendVerificationCodeLetter } from "../../../../common/post";
import gql from "graphql-tag";
+import { ALL_NOTIFICATIONS } from "../../../../users/notifications";
// Mock external search services
jest.mock("../../../search");
@@ -165,14 +166,15 @@ describe("Mutation.createCompany", () => {
})) != null;
expect(newCompanyExists).toBe(true);
- const newCompanyAssociationExists =
- (await prisma.companyAssociation.findFirst({
- where: {
- company: { siret: companyInput.siret },
- user: { id: user.id }
- }
- })) != null;
- expect(newCompanyAssociationExists).toBe(true);
+ const newCompanyAssociation = await prisma.companyAssociation.findFirst({
+ where: {
+ company: { siret: companyInput.siret },
+ user: { id: user.id }
+ }
+ });
+
+ expect(newCompanyAssociation).not.toBeNull();
+ expect(newCompanyAssociation?.notifications).toEqual(ALL_NOTIFICATIONS);
const refreshedUser = await prisma.user.findUniqueOrThrow({
where: { id: user.id }
diff --git a/back/src/companies/resolvers/mutations/__tests__/renewSecurityCode.test.ts b/back/src/companies/resolvers/mutations/__tests__/renewSecurityCode.test.ts
index 9f3369e87d..b3be2a952d 100644
--- a/back/src/companies/resolvers/mutations/__tests__/renewSecurityCode.test.ts
+++ b/back/src/companies/resolvers/mutations/__tests__/renewSecurityCode.test.ts
@@ -23,11 +23,10 @@ jest.mock("../../../../mailer/mailing", () => ({
sendMail: jest.fn((...args) => sendMailMock(...args))
}));
-const getCompanyActiveUsersMock = jest.fn();
+const getNotificationSubscribersMock = jest.fn();
-jest.mock("../../../database", () => ({
- getCompanyActiveUsers: jest.fn(() => getCompanyActiveUsersMock()),
- convertUrls: v => v
+jest.mock("../../../../users/notifications", () => ({
+ getNotificationSubscribers: jest.fn(() => getNotificationSubscribersMock())
}));
describe("renewSecurityCode", () => {
@@ -36,7 +35,7 @@ describe("renewSecurityCode", () => {
updateCompanyMock.mockReset();
(utils.randomNumber as jest.Mock).mockReset();
sendMailMock.mockReset();
- getCompanyActiveUsersMock.mockReset();
+ getNotificationSubscribersMock.mockReset();
});
it("should throw BAD_USER_INPUT exception if siret is not 14 character long", async () => {
@@ -67,12 +66,12 @@ describe("renewSecurityCode", () => {
.mockReturnValueOnce(1234)
.mockReturnValueOnce(2345);
- getCompanyActiveUsersMock.mockReturnValueOnce([]);
+ getNotificationSubscribersMock.mockReturnValueOnce([]);
await renewSecurityCode("85001946400013");
expect(utils.randomNumber as jest.Mock).toHaveBeenCalledTimes(2);
});
- it("should send a notification email to all users and return updated company", async () => {
+ it("should send a notification email to subscribers and return updated company", async () => {
const siret = siretify(2);
companyMock.mockResolvedValueOnce({
securityCode: 1234,
@@ -101,13 +100,13 @@ describe("renewSecurityCode", () => {
}
}
});
- getCompanyActiveUsersMock.mockReturnValueOnce(users);
+ getNotificationSubscribersMock.mockReturnValueOnce(users);
const updatedCompany = await renewSecurityCode(siret);
expect(sendMailMock).toHaveBeenCalledWith(mail);
- expect(updatedCompany).toEqual({
+ expect(updatedCompany).toMatchObject({
orgId: siret,
name: "Code en stock",
securityCode: 4567
diff --git a/back/src/companies/resolvers/mutations/__tests__/updateCompany.integration.ts b/back/src/companies/resolvers/mutations/__tests__/updateCompany.integration.ts
index b761837a95..165876dcbe 100644
--- a/back/src/companies/resolvers/mutations/__tests__/updateCompany.integration.ts
+++ b/back/src/companies/resolvers/mutations/__tests__/updateCompany.integration.ts
@@ -178,21 +178,21 @@ describe("mutation updateCompany", () => {
};
mockGetUpdatedCompanyNameAndAddress.mockResolvedValueOnce({
name: "nom de sirene",
- address: "l'adresse de sirene"
+ address: "l'adresse de sirene",
+ codeNaf: "0710Z"
});
- const { data } = await mutate>(
+ const { data, errors } = await mutate>(
UPDATE_COMPANY,
{
variables
}
);
+ expect(errors).toBeUndefined();
expect(data.updateCompany.id).toEqual(company.id);
expect(data.updateCompany.name).toEqual("nom de sirene");
expect(data.updateCompany.address).toEqual("l'adresse de sirene");
- expect(data.updateCompany.naf).toEqual(company.codeNaf);
- expect(data.updateCompany.libelleNaf).toEqual(
- libelleFromCodeNaf(company.codeNaf!)
- );
+ expect(data.updateCompany.naf).toEqual("0710Z");
+ expect(data.updateCompany.libelleNaf).toEqual(libelleFromCodeNaf("0710Z"));
const updatedCompany = await prisma.company.findUnique({
where: { id: company.id }
@@ -209,8 +209,7 @@ describe("mutation updateCompany", () => {
vatNumber: "RO17579668",
name: "Acme in EU",
address: "Transporter street",
- companyTypes: ["TRANSPORTER"],
- codeNaf: "0112Z"
+ companyTypes: ["TRANSPORTER"]
});
const { mutate } = makeClient({ ...user, auth: AuthType.Session });
@@ -231,10 +230,6 @@ describe("mutation updateCompany", () => {
expect(data.updateCompany.id).toEqual(company.id);
expect(data.updateCompany.name).toEqual("nom de vies");
expect(data.updateCompany.address).toEqual("l'adresse de vies");
- expect(data.updateCompany.naf).toEqual(company.codeNaf);
- expect(data.updateCompany.libelleNaf).toEqual(
- libelleFromCodeNaf(company.codeNaf!)
- );
const updatedCompany = await prisma.company.findUnique({
where: { id: company.id }
diff --git a/back/src/companies/resolvers/mutations/bulkUpdateCompaniesProfiles.ts b/back/src/companies/resolvers/mutations/bulkUpdateCompaniesProfiles.ts
index f21a483787..1f8da5c090 100644
--- a/back/src/companies/resolvers/mutations/bulkUpdateCompaniesProfiles.ts
+++ b/back/src/companies/resolvers/mutations/bulkUpdateCompaniesProfiles.ts
@@ -9,7 +9,7 @@ import { GraphQLContext } from "../../../types";
import { UserRole } from "@prisma/client";
import { bulkUpdateCompanySchema } from "../../validation/schema";
import { UserInputError } from "../../../common/errors";
-import { convertUrls } from "../../../companies/database";
+import { toGqlCompanyPrivate } from "../../converters";
export async function bulkUpdateCompaniesProfiles(
_,
{ input }: MutationBulkUpdateCompaniesProfilesArgs,
@@ -78,7 +78,7 @@ export async function bulkUpdateCompaniesProfiles(
},
where: { orgId: row.orgId }
});
- updatedCompanies.push(convertUrls(updatedCompany));
+ updatedCompanies.push(toGqlCompanyPrivate(updatedCompany));
}
return updatedCompanies;
diff --git a/back/src/companies/resolvers/mutations/createCompany.ts b/back/src/companies/resolvers/mutations/createCompany.ts
index 0f849907f7..50ee87840a 100644
--- a/back/src/companies/resolvers/mutations/createCompany.ts
+++ b/back/src/companies/resolvers/mutations/createCompany.ts
@@ -7,7 +7,6 @@ import {
WasteProcessorType,
WasteVehiclesType
} from "@prisma/client";
-import { convertUrls } from "../../database";
import { prisma } from "@td/prisma";
import { applyAuthStrategies, AuthType } from "../../../auth";
import { sendMail } from "../../../mailer/mailing";
@@ -33,7 +32,8 @@ import { sendVerificationCodeLetter } from "../../../common/post";
import { isGenericEmail } from "@td/constants";
import { parseCompanyAsync } from "../../validation/index";
import { companyInputToZodCompany } from "../../validation/helpers";
-
+import { toGqlCompanyPrivate } from "../../converters";
+import { ALL_NOTIFICATIONS } from "../../../users/notifications";
/**
* Create a new company and associate it to a user
* who becomes the first admin of the company
@@ -185,7 +185,8 @@ const createCompanyResolver: MutationResolvers["createCompany"] = async (
company: {
create: companyCreateInput
},
- role: "ADMIN"
+ role: "ADMIN",
+ notifications: ALL_NOTIFICATIONS
},
include: { company: true }
});
@@ -252,7 +253,7 @@ const createCompanyResolver: MutationResolvers["createCompany"] = async (
) {
await sendFirstOnboardingEmail(companyInput, user);
}
- return convertUrls(company);
+ return toGqlCompanyPrivate(company);
};
export default createCompanyResolver;
diff --git a/back/src/companies/resolvers/mutations/deleteCompany.ts b/back/src/companies/resolvers/mutations/deleteCompany.ts
index 9a445779dc..4ebb2d7c4a 100644
--- a/back/src/companies/resolvers/mutations/deleteCompany.ts
+++ b/back/src/companies/resolvers/mutations/deleteCompany.ts
@@ -1,11 +1,12 @@
import { UserRole } from "@prisma/client";
import { prisma } from "@td/prisma";
import { applyAuthStrategies, AuthType } from "../../../auth";
-import { convertUrls, getCompanyActiveUsers } from "../../database";
+import { getCompanyActiveUsers } from "../../database";
import { checkIsAuthenticated } from "../../../common/permissions";
import { MutationResolvers } from "../../../generated/graphql/types";
import { deleteCachedUserRoles } from "../../../common/redis/users";
import { UserInputError } from "../../../common/errors";
+import { toGqlCompanyPrivate } from "../../converters";
const deleteCompanyResolver: MutationResolvers["deleteCompany"] = async (
_,
@@ -39,7 +40,7 @@ const deleteCompanyResolver: MutationResolvers["deleteCompany"] = async (
await prisma.membershipRequest.deleteMany({ where: { companyId: id } });
await prisma.company.delete({ where: { id } });
- return convertUrls(companyAssociation.company);
+ return toGqlCompanyPrivate(companyAssociation.company);
};
export default deleteCompanyResolver;
diff --git a/back/src/companies/resolvers/mutations/renewSecurityCodeService.ts b/back/src/companies/resolvers/mutations/renewSecurityCodeService.ts
index 41ea546b9d..d3bb3dbaf8 100644
--- a/back/src/companies/resolvers/mutations/renewSecurityCodeService.ts
+++ b/back/src/companies/resolvers/mutations/renewSecurityCodeService.ts
@@ -5,10 +5,12 @@ import { sendMail } from "../../../mailer/mailing";
import { CompanyPrivate } from "../../../generated/graphql/types";
import { randomNumber } from "../../../utils";
-import { convertUrls, getCompanyActiveUsers } from "../../database";
import { renderMail, securityCodeRenewal } from "@td/mail";
import { isSiret, isVat } from "@td/constants";
import { UserInputError } from "../../../common/errors";
+import { getNotificationSubscribers } from "../../../users/notifications";
+import { UserNotification } from "@prisma/client";
+import { toGqlCompanyPrivate } from "../../converters";
/**
* This function is used to renew the security code
@@ -50,19 +52,20 @@ export async function renewSecurityCodeFn(
}
});
- const users = await getCompanyActiveUsers(orgId);
- const recipients = users.map(({ email, name }) => ({
- email,
- name: name ?? ""
- }));
+ const subscribers = await getNotificationSubscribers(
+ UserNotification.SIGNATURE_CODE_RENEWAL,
+ [orgId]
+ );
- const mail = renderMail(securityCodeRenewal, {
- to: recipients,
- variables: {
- company: { orgId: company.orgId, name: company.name }
- }
- });
- sendMail(mail);
+ if (subscribers.length) {
+ const mail = renderMail(securityCodeRenewal, {
+ to: subscribers,
+ variables: {
+ company: { orgId: company.orgId, name: company.name }
+ }
+ });
+ await sendMail(mail);
+ }
- return convertUrls(updatedCompany);
+ return toGqlCompanyPrivate(updatedCompany);
}
diff --git a/back/src/companies/resolvers/mutations/updateCompany.ts b/back/src/companies/resolvers/mutations/updateCompany.ts
index d6565eff40..095cc1c31e 100644
--- a/back/src/companies/resolvers/mutations/updateCompany.ts
+++ b/back/src/companies/resolvers/mutations/updateCompany.ts
@@ -2,7 +2,6 @@ import { MutationResolvers } from "../../../generated/graphql/types";
import { applyAuthStrategies, AuthType } from "../../../auth";
import { checkIsAuthenticated } from "../../../common/permissions";
import {
- convertUrls,
getCompanyOrCompanyNotFound,
getUpdatedCompanyNameAndAddress,
updateFavorites
@@ -10,14 +9,14 @@ import {
import { checkUserPermissions, Permission } from "../../../permissions";
import { NotCompanyAdminErrorMsg } from "../../../common/errors";
-import { libelleFromCodeNaf } from "../../sirene/utils";
-import { Company, Prisma } from "@prisma/client";
+import { Prisma } from "@prisma/client";
import { ZodCompany } from "../../validation/schema";
import { safeInput } from "../../../common/converter";
import { parseCompanyAsync } from "../../validation/index";
import { SiretNotFoundError } from "../../sirene/errors";
import { logger } from "@td/logger";
import { prisma } from "@td/prisma";
+import { toGqlCompanyPrivate } from "../../converters";
const updateCompanyResolver: MutationResolvers["updateCompany"] = async (
parent,
@@ -153,16 +152,10 @@ const updateCompanyResolver: MutationResolvers["updateCompany"] = async (
where: { id: existingCompany.id },
data
});
+
await updateFavorites([existingCompany.orgId]);
- // conversion to CompanyPrivate type
- const naf = (updatedCompany as Company).codeNaf ?? existingCompany.codeNaf;
- const libelleNaf = libelleFromCodeNaf(naf!);
- return {
- ...convertUrls(updatedCompany),
- naf,
- libelleNaf
- };
+ return toGqlCompanyPrivate(updatedCompany);
};
export default updateCompanyResolver;
diff --git a/back/src/companies/resolvers/mutations/verifyCompany.ts b/back/src/companies/resolvers/mutations/verifyCompany.ts
index 31d36c03a1..6f2d89c13e 100644
--- a/back/src/companies/resolvers/mutations/verifyCompany.ts
+++ b/back/src/companies/resolvers/mutations/verifyCompany.ts
@@ -15,13 +15,14 @@ import {
verifiedForeignTransporterCompany
} from "@td/mail";
import { prisma } from "@td/prisma";
-import { convertUrls, getCompanyOrCompanyNotFound } from "../../database";
+import { getCompanyOrCompanyNotFound } from "../../database";
import { isForeignTransporter } from "../../validation";
import { Permission, checkUserPermissions } from "../../../permissions";
import {
NotCompanyAdminErrorMsg,
UserInputError
} from "../../../common/errors";
+import { toGqlCompanyPrivate } from "../../converters";
export const sendFirstOnboardingEmail = async (
{
@@ -94,7 +95,7 @@ const verifyCompanyResolver: MutationResolvers["verifyCompany"] = async (
// Potential onboarding email
await sendFirstOnboardingEmail(verifiedCompany, user);
- return convertUrls(verifiedCompany);
+ return toGqlCompanyPrivate(verifiedCompany);
};
export default verifyCompanyResolver;
diff --git a/back/src/companies/resolvers/queries/__tests__/companyInfos.test.ts b/back/src/companies/resolvers/queries/__tests__/companyInfos.test.ts
index ec309abd0b..3ab63656e4 100644
--- a/back/src/companies/resolvers/queries/__tests__/companyInfos.test.ts
+++ b/back/src/companies/resolvers/queries/__tests__/companyInfos.test.ts
@@ -23,8 +23,7 @@ jest.mock("@td/prisma", () => ({
const installationMock = jest.fn();
jest.mock("../../../database", () => ({
- getInstallation: jest.fn((...args) => installationMock(...args)),
- convertUrls: v => v
+ getInstallation: jest.fn((...args) => installationMock(...args))
}));
describe("companyInfos with SIRET", () => {
diff --git a/back/src/companies/resolvers/queries/__tests__/favorites.integration.ts b/back/src/companies/resolvers/queries/__tests__/favorites.integration.ts
index ab9c3314d3..355739df50 100644
--- a/back/src/companies/resolvers/queries/__tests__/favorites.integration.ts
+++ b/back/src/companies/resolvers/queries/__tests__/favorites.integration.ts
@@ -13,10 +13,10 @@ import {
indexFavorites,
favoritesConstrutor
} from "../../../../queue/jobs/indexFavorites";
-import { convertUrls } from "../../../database";
import { searchCompany } from "../../../search";
import { index, client as elasticSearch } from "../../../../common/elastic";
import { getFormForElastic, indexForm } from "../../../../forms/elastic";
+import { toGqlCompanyPrivate } from "../../../converters";
const request = supertest(app);
@@ -82,7 +82,7 @@ describe("query favorites", () => {
await refreshIndices();
(searchCompany as jest.Mock).mockResolvedValueOnce({
- ...convertUrls(company)
+ ...toGqlCompanyPrivate(company)
});
await indexFavorites(
await favoritesConstrutor({ orgId: company.orgId, type: "RECIPIENT" }),
@@ -137,7 +137,7 @@ describe("query favorites", () => {
await refreshIndices();
(searchCompany as jest.Mock).mockResolvedValueOnce({
- ...convertUrls(company)
+ ...toGqlCompanyPrivate(company)
});
await indexFavorites(
await favoritesConstrutor({ orgId: company.orgId, type: "EMITTER" }),
diff --git a/back/src/companies/sirene/insee/client.ts b/back/src/companies/sirene/insee/client.ts
index 03f207e674..b8c026e018 100644
--- a/back/src/companies/sirene/insee/client.ts
+++ b/back/src/companies/sirene/insee/client.ts
@@ -16,7 +16,8 @@ import {
StatutDiffusionEtablissement
} from "../../../generated/graphql/types";
-const SIRENE_API_BASE_URL = "https://api.insee.fr/entreprises/sirene/V3.11";
+const SIRENE_API_BASE_URL = "https://api.insee.fr/api-sirene/prive/3.11";
+
export const SEARCH_COMPANIES_MAX_SIZE = 20;
/**
diff --git a/back/src/companies/sirene/insee/token.ts b/back/src/companies/sirene/insee/token.ts
index 9afd1d968e..5d4318e175 100644
--- a/back/src/companies/sirene/insee/token.ts
+++ b/back/src/companies/sirene/insee/token.ts
@@ -1,34 +1,46 @@
import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
import { redisClient, setInCache } from "../../../common/redis";
-const SIRENE_API_TOKEN_URL = "https://api.insee.fr/token";
+const SIRENE_API_TOKEN_URL =
+ "https://auth.insee.net/auth/realms/apim-gravitee/protocol/openid-connect/token";
export const INSEE_TOKEN_KEY = "insee_token";
-const { INSEE_SECRET } = process.env;
+const { INSEE_CLIENT_SECRET, INSEE_CLIENT_ID, INSEE_USERNAME, INSEE_PASSWORD } =
+ process.env;
/**
* Generates INSEE Sirene API token
*/
async function generateToken(): Promise {
const headers = {
- Authorization: `Basic ${INSEE_SECRET}`,
"Content-Type": "application/x-www-form-urlencoded"
};
+ // Création des paramètres de la requête avec URLSearchParams
+ const params = new URLSearchParams();
+ params.append("grant_type", "password");
+ params.append("client_id", INSEE_CLIENT_ID);
+ params.append("client_secret", INSEE_CLIENT_SECRET);
+ params.append("username", INSEE_USERNAME);
+ params.append("password", INSEE_PASSWORD);
+
const response = await axios.post<{ access_token: string }>(
SIRENE_API_TOKEN_URL,
- "grant_type=client_credentials",
+ params,
{ headers }
);
return response.data.access_token;
}
+// Le token expire au bout de 300 secondes
+const INSEE_TOKEN_EX = 300;
+
/**
* Generates a token and save it to redis cache
*/
async function renewToken(): Promise {
const token = await generateToken();
- await setInCache(INSEE_TOKEN_KEY, token);
+ await setInCache(INSEE_TOKEN_KEY, token, { EX: INSEE_TOKEN_EX });
}
/**
diff --git a/back/src/companies/sirenify.ts b/back/src/companies/sirenify.ts
index cce4905734..92fdb87777 100644
--- a/back/src/companies/sirenify.ts
+++ b/back/src/companies/sirenify.ts
@@ -140,41 +140,47 @@ export function nextBuildSirenify(
// check if we found a corresponding companySearchResult based on siret
const companySearchResults = await Promise.all(
- accessors.map(({ siret, skip }) =>
- !skip && siret ? searchCompanyFailFast(siret) : null
- )
+ accessors.map(({ siret, skip }) => {
+ if (skip || !siret) {
+ return null;
+ }
+ return searchCompanyFailFast(siret);
+ })
);
// make a copy to avoid mutating initial data
const sirenifiedInput = { ...input };
for (const [idx, companySearchResult] of companySearchResults.entries()) {
+ const { setter } = accessors[idx];
+ if (!companySearchResult) {
+ continue;
+ }
+ const company = companySearchResult as CompanySearchResult;
if (
- !companySearchResult ||
- companySearchResult.statutDiffusionEtablissement ===
- ("P" as StatutDiffusionEtablissement)
- )
+ company.statutDiffusionEtablissement ===
+ ("P" as StatutDiffusionEtablissement)
+ ) {
continue;
- if (companySearchResult.etatAdministratif === "F") {
+ }
+ if (company.etatAdministratif === "F") {
throw new UserInputError(
- `L'établissement ${companySearchResult.siret} est fermé selon le répertoire SIRENE`
+ `L'établissement ${company.siret} est fermé selon le répertoire SIRENE`
);
}
- if (companySearchResult.isDormant) {
+ if (company.isDormant) {
throw new UserInputError(
- `L'établissement ${companySearchResult.siret} est en sommeil sur Trackdéchets. Il n'est pas possible de le mentionner dans un BSD.`
+ `L'établissement ${company.siret} est en sommeil sur Trackdéchets. Il n'est pas possible de le mentionner dans un BSD.`
);
}
- const { setter } = accessors[idx];
-
setter(sirenifiedInput, {
- name: companySearchResult.name,
- address: companySearchResult.address,
- city: companySearchResult.addressCity,
- postalCode: companySearchResult.addressPostalCode,
- street: companySearchResult.addressVoie
+ name: company.name,
+ address: company.address,
+ city: company.addressCity,
+ postalCode: company.addressPostalCode,
+ street: company.addressVoie
});
}
diff --git a/back/src/companies/typeDefs/company.objects.graphql b/back/src/companies/typeDefs/company.objects.graphql
index bb52547b40..86195b6846 100644
--- a/back/src/companies/typeDefs/company.objects.graphql
+++ b/back/src/companies/typeDefs/company.objects.graphql
@@ -45,6 +45,9 @@ type CompanyPrivate {
"Liste des permissions de l'utilisateur authentifié au sein de cet établissement"
userPermissions: [UserPermission!]!
+ "Liste des notifications auquels l'utilisateur est abonné pour cet établissement"
+ userNotifications: [UserNotification!]!
+
"""
Nom d'usage de l'entreprise qui permet de différencier
différents établissements ayant le même nom
@@ -182,8 +185,10 @@ type CompanyPublic {
address: String
"Code commune de l'établissement"
codeCommune: String
- "Nom de l'établissement"
+ "Raison sociale de l'établissement"
name: String
+ "Nom de l'établissement"
+ givenName: String
"Code NAF"
naf: String
"Libellé NAF"
@@ -496,6 +501,7 @@ type EcoOrganisme {
handleBsdasri: Boolean
handleBsda: Boolean
+ handleBsvhu: Boolean
}
"""
@@ -638,6 +644,9 @@ type CompanyMember {
"Si oui ou non cet utilisateur correspond à l'utilisateur authentifié"
isMe: Boolean
+
+ "Liste des notifications auxquelles l'utilisateur est abonné pour cet établissement"
+ notifications: [UserNotification!]!
}
"""
diff --git a/back/src/companies/validation.ts b/back/src/companies/validation.ts
index 8a7a08c571..8ee1b14551 100644
--- a/back/src/companies/validation.ts
+++ b/back/src/companies/validation.ts
@@ -1,5 +1,10 @@
import * as yup from "yup";
-import { Company, CompanyType, WasteProcessorType } from "@prisma/client";
+import {
+ Company,
+ CompanyType,
+ WasteProcessorType,
+ WasteVehiclesType
+} from "@prisma/client";
import {
cleanClue,
isForeignVat,
@@ -31,6 +36,14 @@ export function isWasteVehicles(company: Company) {
return company.companyTypes.includes(CompanyType.WASTE_VEHICLES);
}
+export function isBroyeur(company: Company) {
+ return company.wasteVehiclesTypes.includes(WasteVehiclesType.BROYEUR);
+}
+
+export function isDemolisseur(company: Company) {
+ return company.wasteVehiclesTypes.includes(WasteVehiclesType.DEMOLISSEUR);
+}
+
export function isTransporter({
companyTypes
}: {
@@ -53,6 +66,14 @@ export function isWorker(company: Company) {
return company.companyTypes.includes(CompanyType.WORKER);
}
+export function isBroker(company: Company) {
+ return company.companyTypes.includes(CompanyType.BROKER);
+}
+
+export function isTrader(company: Company) {
+ return company.companyTypes.includes(CompanyType.TRADER);
+}
+
export function hasCremationProfile(company: Company) {
return company.wasteProcessorTypes.includes(WasteProcessorType.CREMATION);
}
diff --git a/back/src/companydigest/developper_doc.md b/back/src/companydigest/developper_doc.md
index 5a3e89ec27..ec997b19dd 100644
--- a/back/src/companydigest/developper_doc.md
+++ b/back/src/companydigest/developper_doc.md
@@ -29,7 +29,7 @@ Chaque établissement est autorisé par un featureFlag.
### Base de données
Le modèle CompanyDigest centralise les données persistés necessaires à cette fonctionalité.
-Le modèle Company reçoit une nouvelle colonne featureFlags (string[]) dans lequel la présence de la chaine `COMPANY_DIGEST` détermine l'accès à la fonctionalité.
+L'accès à la fonctionnalité est ouvert à tous les établissementts depuis le 22/10/2024.
CompanyDigest comporte user pour suivre l'utilisateur à l'origine de la demande
### Webhook
diff --git a/back/src/companydigest/resolvers/mutations/__tests__/companyDigestCreate.integration.ts b/back/src/companydigest/resolvers/mutations/__tests__/companyDigestCreate.integration.ts
index 32e8b741c2..b3c7bbcb0a 100644
--- a/back/src/companydigest/resolvers/mutations/__tests__/companyDigestCreate.integration.ts
+++ b/back/src/companydigest/resolvers/mutations/__tests__/companyDigestCreate.integration.ts
@@ -70,34 +70,8 @@ describe("Mutation.createCompanyDigest", () => {
]);
});
- it("should raise an error if company has no relevant featureFlag", async () => {
- const { user, company } = await userWithCompanyFactory("MEMBER");
-
- const { mutate } = makeClient(user);
- const { errors } = await mutate>(
- CREATE_COMPANY_DIGEST,
- {
- variables: {
- input: { orgId: company.siret, year: new Date().getFullYear() }
- }
- }
- );
-
- expect(errors).toEqual([
- expect.objectContaining({
- message:
- "Vous nêtes pas autorisé à utiliser cette fonctionalité sur cet établissement.",
- extensions: expect.objectContaining({
- code: ErrorCode.BAD_USER_INPUT
- })
- })
- ]);
- });
-
it("should raise an error if year is too old", async () => {
- const { user, company } = await userWithCompanyFactory("MEMBER", {
- featureFlags: ["COMPANY_DIGEST"]
- });
+ const { user, company } = await userWithCompanyFactory("MEMBER");
const { mutate } = makeClient(user);
const year = new Date().getFullYear() - 2;
@@ -122,9 +96,7 @@ describe("Mutation.createCompanyDigest", () => {
});
it("should create a companyDigest for current year", async () => {
- const { user, company } = await userWithCompanyFactory("MEMBER", {
- featureFlags: ["COMPANY_DIGEST"]
- });
+ const { user, company } = await userWithCompanyFactory("MEMBER");
const year = new Date().getFullYear();
const { mutate } = makeClient(user);
@@ -142,9 +114,7 @@ describe("Mutation.createCompanyDigest", () => {
});
it("should create a companyDigest for last year", async () => {
- const { user, company } = await userWithCompanyFactory("MEMBER", {
- featureFlags: ["COMPANY_DIGEST"]
- });
+ const { user, company } = await userWithCompanyFactory("MEMBER");
const year = new Date().getFullYear() - 1;
const { mutate } = makeClient(user);
diff --git a/back/src/companydigest/resolvers/mutations/create.ts b/back/src/companydigest/resolvers/mutations/create.ts
index fc102962ce..27d78f84b1 100644
--- a/back/src/companydigest/resolvers/mutations/create.ts
+++ b/back/src/companydigest/resolvers/mutations/create.ts
@@ -11,7 +11,6 @@ import { prisma } from "@td/prisma";
import { sendGericoApiRequest } from "../../../queue/producers/gerico";
import { applyAuthStrategies, AuthType } from "../../../auth";
import { CompanyDigestStatus } from "@prisma/client";
-const COMPANY_DIGEST_FLAG = "COMPANY_DIGEST";
const createCompanyDigestResolver = async (
_: ResolversParentTypes["Mutation"],
@@ -54,16 +53,6 @@ const createCompanyDigestResolver = async (
);
}
- const companyHasFeatureFlag = await prisma.company.findFirst({
- where: { orgId, featureFlags: { has: COMPANY_DIGEST_FLAG } },
- select: { id: true }
- });
-
- if (!companyHasFeatureFlag) {
- throw new UserInputError(
- "Vous nêtes pas autorisé à utiliser cette fonctionalité sur cet établissement."
- );
- }
const companyDigest = await prisma.companyDigest.create({
data: {
orgId,
diff --git a/back/src/forms/__tests__/elastic.integration.ts b/back/src/forms/__tests__/elastic.integration.ts
index 59236d261f..5dcbdd4e63 100644
--- a/back/src/forms/__tests__/elastic.integration.ts
+++ b/back/src/forms/__tests__/elastic.integration.ts
@@ -1,4 +1,9 @@
-import { Company, Status } from "@prisma/client";
+import {
+ Company,
+ EmptyReturnADR,
+ Status,
+ WasteAcceptationStatus
+} from "@prisma/client";
import { resetDatabase } from "../../../integration-tests/helper";
import { prisma } from "@td/prisma";
import {
@@ -9,10 +14,15 @@ import {
userFactory,
userWithCompanyFactory
} from "../../__tests__/factories";
-import { getFirstTransporterSync, getFullForm } from "../database";
+import {
+ getFirstTransporterSync,
+ getFullForm,
+ getLastTransporterSync
+} from "../database";
import { getSiretsByTab } from "../elasticHelpers";
import { getFormForElastic, toBsdElastic } from "../elastic";
import { BsdElastic } from "../../common/elastic";
+import { xDaysAgo } from "../../utils";
describe("getSiretsByTab", () => {
afterEach(resetDatabase);
@@ -368,6 +378,160 @@ describe("getSiretsByTab", () => {
forwardedInTransporter!.transporterCompanySiret
);
});
+
+ describe("isReturnFor", () => {
+ it.each([
+ WasteAcceptationStatus.REFUSED,
+ WasteAcceptationStatus.PARTIALLY_REFUSED
+ ])(
+ "waste acceptation status is %p > bsdd should belong to tab",
+ async wasteAcceptationStatus => {
+ // Given
+ const user = await userFactory();
+ const form = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: Status.RECEIVED,
+ receivedAt: new Date(),
+ wasteAcceptationStatus
+ }
+ });
+ const formForElastic = await getFormForElastic(form);
+ const transporter = getLastTransporterSync(formForElastic);
+
+ // When
+ const { isReturnFor } = toBsdElastic(formForElastic);
+
+ // Then
+ expect(isReturnFor).toContain(transporter?.transporterCompanySiret);
+ }
+ );
+
+ it("status is REFUSED > bsdd should belong to tab", async () => {
+ // Given
+ const user = await userFactory();
+ const form = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: Status.REFUSED,
+ receivedAt: new Date()
+ }
+ });
+ const fullForm = await getFormForElastic(form);
+ const transporter = getLastTransporterSync(fullForm);
+
+ // When
+ const { isReturnFor } = toBsdElastic(fullForm);
+
+ // Then
+ expect(isReturnFor).toContain(transporter?.transporterCompanySiret);
+ });
+
+ it("waste acceptation status is ACCEPTED > bsdd should not belong to tab", async () => {
+ // Given
+ const user = await userFactory();
+ const form = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: Status.RECEIVED,
+ receivedAt: new Date(),
+ wasteAcceptationStatus: WasteAcceptationStatus.ACCEPTED
+ }
+ });
+ const fullForm = await getFormForElastic(form);
+
+ // When
+ const { isReturnFor } = toBsdElastic(fullForm);
+
+ // Then
+ expect(isReturnFor).toStrictEqual([]);
+ });
+
+ it("form has been received too long ago > should not belong to tab", async () => {
+ // Given
+ const user = await userFactory();
+ const form = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: Status.RECEIVED,
+ receivedAt: xDaysAgo(new Date(), 10),
+ wasteAcceptationStatus: WasteAcceptationStatus.REFUSED
+ }
+ });
+ const fullForm = await getFormForElastic(form);
+
+ // When
+ const { isReturnFor } = toBsdElastic(fullForm);
+
+ // Then
+ expect(isReturnFor).toStrictEqual([]);
+ });
+
+ it("empty ADR stuff has been precised > bsdd should belong to tab", async () => {
+ // Given
+ const user = await userFactory();
+ const form = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: Status.RECEIVED,
+ receivedAt: new Date(),
+ emptyReturnADR: EmptyReturnADR.EMPTY_CITERNE
+ }
+ });
+ const fullForm = await getFormForElastic(form);
+ const transporter = getLastTransporterSync(fullForm);
+
+ // When
+ const { isReturnFor } = toBsdElastic(fullForm);
+
+ // Then
+ expect(isReturnFor).toContain(transporter?.transporterCompanySiret);
+ });
+
+ it("citerne stuff has been precised > bsdd should belong to tab", async () => {
+ // Given
+ const user = await userFactory();
+ const form = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: Status.RECEIVED,
+ receivedAt: new Date(),
+ hasCiterneBeenWashedOut: false
+ }
+ });
+ const fullForm = await getFormForElastic(form);
+ const transporter = getLastTransporterSync(fullForm);
+
+ // When
+ const { isReturnFor } = toBsdElastic(fullForm);
+
+ // Then
+ expect(isReturnFor).toContain(transporter?.transporterCompanySiret);
+ });
+
+ it("combination of all > bsdd should belong to tab", async () => {
+ // Given
+ const user = await userFactory();
+ const form = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: Status.RECEIVED,
+ receivedAt: new Date(),
+ wasteAcceptationStatus: WasteAcceptationStatus.REFUSED,
+ hasCiterneBeenWashedOut: false,
+ emptyReturnADR: EmptyReturnADR.EMPTY_RETURN_NOT_WASHED
+ }
+ });
+ const fullForm = await getFormForElastic(form);
+ const transporter = getLastTransporterSync(fullForm);
+
+ // When
+ const { isReturnFor } = toBsdElastic(fullForm);
+
+ // Then
+ expect(isReturnFor).toContain(transporter?.transporterCompanySiret);
+ });
+ });
});
describe("toBsdElastic > companies Names & OrgIds", () => {
diff --git a/back/src/forms/database.ts b/back/src/forms/database.ts
index 526cee9b92..d4c2a2ba7a 100644
--- a/back/src/forms/database.ts
+++ b/back/src/forms/database.ts
@@ -217,6 +217,15 @@ export function getFirstTransporterSync(form: {
return firstTransporter ?? null;
}
+export function getLastTransporterSync(form: {
+ transporters: BsddTransporter[] | null;
+}): BsddTransporter | null {
+ const transporters = getTransportersSync(form);
+ const greatestNumber = Math.max(...transporters.map(t => t.number));
+ const lastTransporter = transporters.find(t => t.number === greatestNumber);
+ return lastTransporter ?? null;
+}
+
// Renvoie le premier transporteur qui n'a pas encor signé
export function getNextTransporterSync(form: {
transporters: BsddTransporter[] | null;
diff --git a/back/src/forms/elastic.ts b/back/src/forms/elastic.ts
index 088ba5cddd..8d199dc75d 100644
--- a/back/src/forms/elastic.ts
+++ b/back/src/forms/elastic.ts
@@ -17,7 +17,8 @@ import { getRegistryFields } from "./registry";
import {
getSiretsByTab,
getRecipient,
- getFormRevisionOrgIds
+ getFormRevisionOrgIds,
+ getFormReturnOrgIds
} from "./elasticHelpers";
import { buildAddress } from "../companies/sirene/utils";
@@ -135,6 +136,7 @@ export function toBsdElastic(form: FormForElastic): BsdElastic {
? form.quantityReceived.toNumber()
: null,
destinationOperationDate: form.processedAt?.getTime(),
+ ...getFormReturnOrgIds(form),
...(form.forwarding
? {
// do not display BSD suite in dashboard
diff --git a/back/src/forms/elasticHelpers.ts b/back/src/forms/elasticHelpers.ts
index 3f074f8328..a5d329a92b 100644
--- a/back/src/forms/elasticHelpers.ts
+++ b/back/src/forms/elasticHelpers.ts
@@ -1,15 +1,23 @@
-import { BsddTransporter, EmitterType, Status } from "@prisma/client";
+import {
+ BsddTransporter,
+ EmitterType,
+ Status,
+ WasteAcceptationStatus
+} from "@prisma/client";
import { BsdElastic } from "../common/elastic";
import { FullForm } from "./types";
import { getTransporterCompanyOrgId } from "@td/constants";
import {
getFirstTransporterSync,
+ getLastTransporterSync,
getNextTransporterSync,
getTransportersSync
} from "./database";
import { FormForElastic } from "./elastic";
import { getRevisionOrgIds } from "../common/elasticHelpers";
+import { isDefined } from "../common/helpers";
+import { xDaysAgo } from "../utils";
/**
* Computes which SIRET or VAT number should appear on which tab in the frontend
@@ -223,6 +231,40 @@ export function getSiretsByTab(form: FullForm): Pick {
return siretsByTab;
}
+/**
+ * BSDD belongs to isReturnFor tab if:
+ * - waste has been received in the last 48 hours
+ * - waste has citerne business (emptyReturnADR or citerne washed) OR waste hasn't been fully accepted
+ */
+export const belongsToIsReturnForTab = (form: FullForm) => {
+ const hasBeenReceivedLately =
+ isDefined(form.receivedAt) && form.receivedAt! > xDaysAgo(new Date(), 2);
+
+ if (!hasBeenReceivedLately) return false;
+
+ const hasCiterneBusiness =
+ isDefined(form.emptyReturnADR) || isDefined(form.hasCiterneBeenWashedOut);
+
+ const hasNotBeenFullyAccepted =
+ form.status === Status.REFUSED ||
+ form.wasteAcceptationStatus !== WasteAcceptationStatus.ACCEPTED;
+
+ return hasCiterneBusiness || hasNotBeenFullyAccepted;
+};
+
+export function getFormReturnOrgIds(form: FullForm) {
+ // Return tab
+ if (belongsToIsReturnForTab(form)) {
+ const lastTransporter = getLastTransporterSync(form);
+
+ return {
+ isReturnFor: [lastTransporter?.transporterCompanySiret].filter(Boolean)
+ };
+ }
+
+ return { isReturnFor: [] };
+}
+
function getFormSirets(form: FullForm) {
const transporter = getFirstTransporterSync(form);
diff --git a/back/src/forms/examples/fixtures.ts b/back/src/forms/examples/fixtures.ts
index 2af213d5e9..1fbfc13952 100644
--- a/back/src/forms/examples/fixtures.ts
+++ b/back/src/forms/examples/fixtures.ts
@@ -2,8 +2,6 @@
* Fixtures used as building blocks of mutations inputs
*/
-import { TransportMode } from "@prisma/client";
-
function emitterCompanyInput(siret: string) {
return {
siret,
@@ -131,7 +129,7 @@ function signTransportFormInput() {
takenOverAt: "2020-04-03T14:48:00",
takenOverBy: "Isabelle Guichard",
transporterNumberPlate: "AA-123456-BB",
- transporterTransportMode: TransportMode.ROAD
+ transporterTransportMode: "ROAD"
};
}
diff --git a/back/src/forms/mail/__tests__/renderFormRefusedEmail.integration.ts b/back/src/forms/mail/__tests__/renderFormRefusedEmail.integration.ts
index 0b70e1fdca..68e2ea47a4 100644
--- a/back/src/forms/mail/__tests__/renderFormRefusedEmail.integration.ts
+++ b/back/src/forms/mail/__tests__/renderFormRefusedEmail.integration.ts
@@ -345,8 +345,8 @@ describe("renderFormRefusedEmail", () => {
{ email: emitter.user.email, name: emitter.user.name }
]);
expect(email!.cc).toEqual([
- { email: destination.user.email, name: destination.user.name },
- { email: ttr.user.email, name: ttr.user.name }
+ { email: ttr.user.email, name: ttr.user.name },
+ { email: destination.user.email, name: destination.user.name }
]);
expect(email!.body).toContain(`
Nous vous informons que la société ${destination.company.name}
diff --git a/back/src/forms/mail/renderFormRefusedEmail.ts b/back/src/forms/mail/renderFormRefusedEmail.ts
index faf7b85549..dec3af0f98 100644
--- a/back/src/forms/mail/renderFormRefusedEmail.ts
+++ b/back/src/forms/mail/renderFormRefusedEmail.ts
@@ -1,6 +1,5 @@
-import { BsddTransporter, Form } from "@prisma/client";
+import { BsddTransporter, Form, UserNotification } from "@prisma/client";
import { prisma } from "@td/prisma";
-import { getCompanyAdminUsers } from "../../companies/database";
import { generateBsddPdfToBase64 } from "../pdf";
import {
Mail,
@@ -11,6 +10,7 @@ import {
import { getTransporterCompanyOrgId, Dreals } from "@td/constants";
import { getFirstTransporter } from "../database";
import { bsddWasteQuantities } from "../helpers/bsddWasteQuantities";
+import { getNotificationSubscribers } from "../../users/notifications";
const { NOTIFY_DREAL_WHEN_FORM_DECLINED } = process.env;
@@ -42,14 +42,6 @@ export async function renderFormRefusedEmail(
name: `${filename}.pdf`
};
- const emitterCompanyAdmins = await getCompanyAdminUsers(
- form.emitterCompanySiret!
- );
- const destinationCompanyAdmins = await getCompanyAdminUsers(destinationSiret);
- const tempStorerCompanyAdmins = isFinalDestinationRefusal
- ? await getCompanyAdminUsers(form.recipientCompanySiret!)
- : [];
-
let drealsRecipients: typeof Dreals = [];
if (notifyDreal) {
@@ -66,17 +58,23 @@ export async function renderFormRefusedEmail(
drealsRecipients = Dreals.filter(d => formDepartments.includes(d.Dept));
}
- const to = emitterCompanyAdmins.map(admin => ({
- email: admin.email,
- name: admin.name ?? ""
- }));
+ const to = await getNotificationSubscribers(
+ UserNotification.BSD_REFUSAL,
+ [form.emitterCompanySiret].filter(Boolean)
+ );
+
+ const destinationCC = await getNotificationSubscribers(
+ UserNotification.BSD_REFUSAL,
+ [
+ form.recipientCompanySiret,
+ isFinalDestinationRefusal ? forwardedIn?.recipientCompanySiret : null
+ ].filter(Boolean)
+ );
// include drealsRecipients if settings says so
- const cc = [
- ...destinationCompanyAdmins,
- ...tempStorerCompanyAdmins,
- ...(notifyDreal ? drealsRecipients : [])
- ].map(admin => ({ email: admin.email, name: admin.name ?? "" }));
+ const cc = [...destinationCC, ...(notifyDreal ? drealsRecipients : [])].map(
+ admin => ({ email: admin.email, name: admin.name ?? "" })
+ );
// Get formNotAccepted or formPartiallyRefused mail function according to wasteAcceptationStatus value
const mailTemplate = {
diff --git a/back/src/forms/repository/formRevisionRequest/acceptRevisionRequestApproval.ts b/back/src/forms/repository/formRevisionRequest/acceptRevisionRequestApproval.ts
index ed605d47b1..c5df93706a 100644
--- a/back/src/forms/repository/formRevisionRequest/acceptRevisionRequestApproval.ts
+++ b/back/src/forms/repository/formRevisionRequest/acceptRevisionRequestApproval.ts
@@ -26,6 +26,7 @@ import { distinct } from "../../../common/arrays";
import { ForbiddenError } from "../../../common/errors";
import { isFinalOperationCode } from "../../../common/operationCodes";
import { operationHook } from "../../operationHook";
+import { areDefined } from "../../../common/helpers";
export type AcceptRevisionRequestApprovalFn = (
revisionRequestApprovalId: string,
@@ -176,6 +177,8 @@ async function getUpdateFromFormRevisionRequest(
...(revisionRequest.wasteDetailsPop !== null && {
wasteDetailsPop: revisionRequest.wasteDetailsPop
}),
+ wasteDetailsQuantity: revisionRequest.wasteDetailsQuantity,
+ wasteDetailsSampleNumber: revisionRequest.wasteDetailsSampleNumber,
...(revisionRequest.wasteDetailsPackagingInfos && {
wasteDetailsPackagingInfos: revisionRequest.wasteDetailsPackagingInfos
}),
@@ -363,6 +366,46 @@ export async function approveAndApplyRevisionRequest(
}
}
+ // Revision targeted an ANNEXE_1 (child). We need to update its parent
+ if (updatedBsdd.emitterType === EmitterType.APPENDIX1_PRODUCER) {
+ // Quantity changed. We need to fix parent's quantity as well
+ if (
+ areDefined(
+ bsddBeforeRevision.wasteDetailsQuantity,
+ revisionRequest.wasteDetailsQuantity
+ )
+ ) {
+ // Calculate revision diff
+ const diff = bsddBeforeRevision
+ .wasteDetailsQuantity!.minus(revisionRequest.wasteDetailsQuantity!)
+ .toDecimalPlaces(6);
+ const { nextForm: parent } = await prisma.formGroupement.findFirstOrThrow(
+ {
+ where: { initialFormId: bsddBeforeRevision.id },
+ include: {
+ nextForm: true
+ }
+ }
+ );
+
+ // Calculate parent new quantity using diff
+ const parentNewQuantity = parent.wasteDetailsQuantity
+ ?.minus(diff)
+ .toDecimalPlaces(6);
+
+ // Update & re-index parent
+ await prisma.form.update({
+ where: { id: parent.id },
+ data: {
+ wasteDetailsQuantity: parentNewQuantity
+ }
+ });
+ prisma.addAfterCommitCallback?.(() => {
+ enqueueUpdatedBsdToIndex(parent.readableId);
+ });
+ }
+ }
+
if (updatedBsdd.emitterType === EmitterType.APPENDIX1) {
const { wasteDetailsCode, wasteDetailsName, wasteDetailsPop } =
revisionRequest;
diff --git a/back/src/forms/resolvers/mutations/__tests__/markAsAccepted.integration.ts b/back/src/forms/resolvers/mutations/__tests__/markAsAccepted.integration.ts
index eb274f15f1..c9e9c49a19 100644
--- a/back/src/forms/resolvers/mutations/__tests__/markAsAccepted.integration.ts
+++ b/back/src/forms/resolvers/mutations/__tests__/markAsAccepted.integration.ts
@@ -5,6 +5,7 @@ import {
EmptyReturnADR,
Status,
TransportMode,
+ UserNotification,
UserRole,
WasteAcceptationStatus
} from "@prisma/client";
@@ -29,6 +30,7 @@ import { generateBsddPdfToBase64 } from "../../../pdf/generateBsddPdf";
import getReadableId from "../../../readableId";
import { updateAppendix2Queue } from "../../../../queue/producers/updateAppendix2";
import { waitForJobsCompletion } from "../../../../queue/helpers";
+import { associateUserToCompany } from "../../../../users/database";
// No mails
jest.mock("../../../../mailer/mailing");
@@ -241,8 +243,32 @@ describe("Test Form reception", () => {
});
it("should mark a received form as refused", async () => {
- const { emitterCompany, recipient, recipientCompany, form } =
+ const { emitter, emitterCompany, recipient, recipientCompany, form } =
await prepareDB();
+
+ // should received email
+ const emitter2 = await userFactory();
+ await associateUserToCompany(emitter2.id, emitterCompany.orgId, "ADMIN", {
+ notifications: [UserNotification.BSD_REFUSAL]
+ });
+
+ // should not receive email
+ const emitter3 = await userFactory();
+ await associateUserToCompany(emitter3.id, emitterCompany.orgId, "ADMIN", {
+ notifications: []
+ });
+
+ // should received email
+ const recipient2 = await userFactory();
+ await associateUserToCompany(
+ recipient2.id,
+ recipientCompany.orgId,
+ "ADMIN",
+ {
+ notifications: [UserNotification.BSD_REFUSAL]
+ }
+ );
+
await prisma.form.update({
where: { id: form.id },
data: {
@@ -289,14 +315,57 @@ describe("Test Form reception", () => {
expect(sendMail as jest.Mock).toHaveBeenCalledWith(
expect.objectContaining({
subject:
- "Le déchet de l’entreprise company_1 a été totalement refusé à réception"
+ "Le déchet de l’entreprise company_1 a été totalement refusé à réception",
+ to: [
+ { email: emitter.email, name: emitter.name },
+ { email: emitter2.email, name: emitter2.name }
+ ],
+ cc: [
+ { email: recipient.email, name: recipient.name },
+ { email: recipient2.email, name: recipient2.name }
+ ]
})
);
});
- it("should mark a received form as partially refused", async () => {
- const { emitterCompany, recipient, recipientCompany, form } =
+ it("should mark a received form as partially refused and send emails to subscribers", async () => {
+ const { emitter, emitterCompany, recipient, recipientCompany, form } =
await prepareDB();
+
+ // should received email
+ const emitter2 = await userFactory();
+ await associateUserToCompany(emitter2.id, emitterCompany.orgId, "ADMIN", {
+ notifications: [UserNotification.BSD_REFUSAL]
+ });
+
+ // should not receive email
+ const emitter3 = await userFactory();
+ await associateUserToCompany(emitter3.id, emitterCompany.orgId, "ADMIN", {
+ notifications: []
+ });
+
+ // should received email
+ const recipient2 = await userFactory();
+ await associateUserToCompany(
+ recipient2.id,
+ recipientCompany.orgId,
+ "ADMIN",
+ {
+ notifications: [UserNotification.BSD_REFUSAL]
+ }
+ );
+
+ // should not receive email
+ const recipient3 = await userFactory();
+ await associateUserToCompany(
+ recipient3.id,
+ recipientCompany.orgId,
+ "ADMIN",
+ {
+ notifications: []
+ }
+ );
+
await prisma.form.update({
where: { id: form.id },
data: {
@@ -342,7 +411,15 @@ describe("Test Form reception", () => {
expect(sendMail as jest.Mock).toHaveBeenCalledWith(
expect.objectContaining({
subject:
- "Le déchet de l’entreprise company_1 a été partiellement refusé à réception"
+ "Le déchet de l’entreprise company_1 a été partiellement refusé à réception",
+ to: [
+ { email: emitter.email, name: emitter.name },
+ { email: emitter2.email, name: emitter2.name }
+ ],
+ cc: [
+ { email: recipient.email, name: recipient.name },
+ { email: recipient2.email, name: recipient2.name }
+ ]
})
);
});
diff --git a/back/src/forms/resolvers/mutations/__tests__/markAsProcessed.integration.ts b/back/src/forms/resolvers/mutations/__tests__/markAsProcessed.integration.ts
index 54b3653399..97b184d819 100644
--- a/back/src/forms/resolvers/mutations/__tests__/markAsProcessed.integration.ts
+++ b/back/src/forms/resolvers/mutations/__tests__/markAsProcessed.integration.ts
@@ -1259,7 +1259,9 @@ describe("mutation.markAsProcessed", () => {
status: "GROUPED",
quantityReceived: 0.02
},
- forwardedInOpts: { quantityReceived: 0.007 }
+ forwardedInOpts: {
+ quantityReceived: 0.007
+ }
});
const form = await formFactory({
@@ -1317,6 +1319,90 @@ describe("mutation.markAsProcessed", () => {
expect(updatedGroupedForm2.status).toEqual("PROCESSED");
});
+ it("should mark partially accepted appendix2 forms as processed", async () => {
+ const { user, company } = await userWithCompanyFactory("ADMIN");
+
+ const groupedForm1 = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: "GROUPED",
+ // quantité acceptée = 0.52
+ quantityReceived: 1,
+ quantityRefused: 0.48,
+ wasteAcceptationStatus: "PARTIALLY_REFUSED"
+ }
+ });
+
+ // it should also work for BSD with temporary storage
+ const groupedForm2 = await formWithTempStorageFactory({
+ ownerId: user.id,
+ opt: {
+ status: "GROUPED",
+ quantityReceived: 1
+ },
+ forwardedInOpts: {
+ // quantité acceptée = 0.52
+ quantityReceived: 1,
+ quantityRefused: 0.48,
+ wasteAcceptationStatus: "PARTIALLY_REFUSED"
+ }
+ });
+
+ const form = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: "ACCEPTED",
+ emitterType: "APPENDIX2",
+ recipientCompanyName: company.name,
+ recipientCompanySiret: company.siret,
+ grouping: {
+ create: [
+ {
+ initialFormId: groupedForm1.id,
+ quantity: 0.52
+ },
+ {
+ initialFormId: groupedForm2.id,
+ quantity: 0.52
+ }
+ ]
+ }
+ }
+ });
+
+ const { mutate } = makeClient(user);
+
+ const mutateFn = () =>
+ mutate(MARK_AS_PROCESSED, {
+ variables: {
+ id: form.id,
+ processedInfo: {
+ processingOperationDescription: "Une description",
+ processingOperationDone: "D 1",
+ destinationOperationMode: OperationMode.ELIMINATION,
+ processedBy: "A simple bot",
+ processedAt: "2018-12-11T00:00:00.000Z"
+ }
+ }
+ });
+
+ await waitForJobsCompletion({
+ fn: mutateFn,
+ queue: updateAppendix2Queue,
+ expectedJobCount: 2
+ });
+
+ const updatedGroupedForm1 = await prisma.form.findUniqueOrThrow({
+ where: { id: groupedForm1.id }
+ });
+ expect(updatedGroupedForm1.status).toEqual("PROCESSED");
+
+ const updatedGroupedForm2 = await prisma.form.findUniqueOrThrow({
+ where: { id: groupedForm2.id }
+ });
+ expect(updatedGroupedForm2.status).toEqual("PROCESSED");
+ });
+
it("should mark appendix2 forms as processed recursively", async () => {
const { user, company } = await userWithCompanyFactory("ADMIN");
diff --git a/back/src/forms/resolvers/mutations/__tests__/markAsSealed.integration.ts b/back/src/forms/resolvers/mutations/__tests__/markAsSealed.integration.ts
index 03a0b9e518..314af03310 100644
--- a/back/src/forms/resolvers/mutations/__tests__/markAsSealed.integration.ts
+++ b/back/src/forms/resolvers/mutations/__tests__/markAsSealed.integration.ts
@@ -16,7 +16,7 @@ import {
} from "../../../../__tests__/factories";
import makeClient from "../../../../__tests__/testClient";
import { sendMail } from "../../../../mailer/mailing";
-import { renderMail, contentAwaitsGuest } from "@td/mail";
+import { renderMail, yourCompanyIsIdentifiedOnABsd } from "@td/mail";
import { MARK_AS_SEALED } from "./mutations";
import { updateAppendix2Queue } from "../../../../queue/producers/updateAppendix2";
import { waitForJobsCompletion } from "../../../../queue/helpers";
@@ -735,6 +735,80 @@ describe("Mutation.markAsSealed", () => {
expect(updatedGroupedForm2.status).toEqual("GROUPED");
});
+ it("should mark partially accepted appendix2 forms as grouped", async () => {
+ const { user, company } = await userWithCompanyFactory("MEMBER");
+ const destination = await destinationFactory();
+ const groupedForm1 = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: "AWAITING_GROUP",
+ // quantité acceptée = 0.52
+ quantityReceived: 1,
+ quantityRefused: 0.48,
+ wasteAcceptationStatus: "PARTIALLY_REFUSED"
+ }
+ });
+
+ // it should also work for BSD with temporary storage
+ const groupedForm2 = await formWithTempStorageFactory({
+ ownerId: user.id,
+ opt: {
+ status: "GROUPED",
+ quantityReceived: 0.02
+ },
+ forwardedInOpts: {
+ // quantité acceptée = 0.52
+ quantityReceived: 1,
+ quantityRefused: 0.48,
+ wasteAcceptationStatus: "PARTIALLY_REFUSED"
+ }
+ });
+
+ const form = await formFactory({
+ ownerId: user.id,
+ opt: {
+ status: "DRAFT",
+ emitterType: "APPENDIX2",
+ emitterCompanySiret: company.siret,
+ recipientCompanySiret: destination.siret,
+ grouping: {
+ create: [
+ {
+ initialFormId: groupedForm1.id,
+ quantity: 0.52
+ },
+ {
+ initialFormId: groupedForm2.id,
+ quantity: 0.52
+ }
+ ]
+ }
+ }
+ });
+
+ const { mutate } = makeClient(user);
+
+ const mutateFn = () =>
+ mutate(MARK_AS_SEALED, {
+ variables: { id: form.id }
+ });
+
+ await waitForJobsCompletion({
+ fn: mutateFn,
+ queue: updateAppendix2Queue,
+ expectedJobCount: 2
+ });
+
+ const updatedGroupedForm1 = await prisma.form.findUniqueOrThrow({
+ where: { id: groupedForm1.id }
+ });
+ expect(updatedGroupedForm1.status).toEqual("GROUPED");
+ const updatedGroupedForm2 = await prisma.form.findUniqueOrThrow({
+ where: { id: groupedForm2.id }
+ });
+ expect(updatedGroupedForm2.status).toEqual("GROUPED");
+ });
+
it("should mark appendix2 forms as grouped despite rogue decimal digits", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
const destination = await destinationFactory();
@@ -1073,19 +1147,26 @@ describe("Mutation.markAsSealed", () => {
variables: { id: form.id }
});
- expect(sendMail as jest.Mock).toHaveBeenCalledWith(
- renderMail(contentAwaitsGuest, {
+ const expectedMail = {
+ ...renderMail(yourCompanyIsIdentifiedOnABsd, {
to: [
{ email: form.emitterCompanyMail!, name: form.emitterCompanyContact! }
],
variables: {
- company: {
+ emitter: {
siret: form.emitterCompanySiret!,
name: form.emitterCompanyName!
+ },
+ destination: {
+ siret: form.recipientCompanySiret!,
+ name: form.recipientCompanyName!
}
}
- })
- );
+ }),
+ params: { hideRegisteredUserInfo: true }
+ };
+
+ expect(sendMail as jest.Mock).toHaveBeenCalledWith(expectedMail);
});
it("should not send an email to emitter contact if contact email already appears in a previous BSD", async () => {
diff --git a/back/src/forms/resolvers/mutations/__tests__/submitFormRevisionRequestApproval.integration.ts b/back/src/forms/resolvers/mutations/__tests__/submitFormRevisionRequestApproval.integration.ts
index a7e5e68b7d..b6346f503e 100644
--- a/back/src/forms/resolvers/mutations/__tests__/submitFormRevisionRequestApproval.integration.ts
+++ b/back/src/forms/resolvers/mutations/__tests__/submitFormRevisionRequestApproval.integration.ts
@@ -16,6 +16,7 @@ import makeClient from "../../../../__tests__/testClient";
import {
CompanyType,
EmitterType,
+ RevisionRequestStatus,
Status,
UserRole,
WasteAcceptationStatus
@@ -960,6 +961,321 @@ describe("Mutation.submitFormRevisionRequestApproval", () => {
expect(updatedAppendix1.wasteDetailsCode).toBe(newWasteCode);
});
+ it("should udate appendix 1 parent's quantity if revision modified child's quantity", async () => {
+ // Given
+ const { company: childAppendixEmitter } = await userWithCompanyFactory(
+ "ADMIN"
+ );
+ const { user: parentAppendixUser, company: parentAppendixEmitter } =
+ await userWithCompanyFactory("ADMIN");
+ const { company: transporter } = await userWithCompanyFactory("ADMIN");
+
+ const appendix1Child = await prisma.form.create({
+ data: {
+ readableId: getReadableId(),
+ status: Status.RECEIVED,
+ emitterType: EmitterType.APPENDIX1_PRODUCER,
+ emitterCompanySiret: childAppendixEmitter.siret,
+ wasteDetailsCode: "15 01 10*",
+ wasteDetailsQuantity: 10,
+ owner: { connect: { id: parentAppendixUser.id } },
+ transporters: {
+ create: {
+ number: 1,
+ transporterCompanySiret: transporter.siret
+ }
+ }
+ }
+ });
+
+ const appendix1Parent = await formFactory({
+ ownerId: parentAppendixUser.id,
+ opt: {
+ status: Status.SENT,
+ emitterType: EmitterType.APPENDIX1,
+ emitterCompanySiret: parentAppendixEmitter.siret,
+ emitterCompanyName: parentAppendixEmitter.name,
+ recipientCompanySiret: parentAppendixEmitter.siret,
+ wasteDetailsQuantity: 10,
+ grouping: {
+ create: { initialFormId: appendix1Child.id, quantity: 0 }
+ },
+ transporters: {
+ create: {
+ number: 1,
+ transporterCompanySiret: transporter.siret
+ }
+ }
+ }
+ });
+
+ // When
+ const revisionRequest = await prisma.bsddRevisionRequest.create({
+ data: {
+ bsddId: appendix1Child.id,
+ authoringCompanyId: childAppendixEmitter.id,
+ approvals: { create: { approverSiret: parentAppendixEmitter.siret! } },
+ wasteDetailsQuantity: 6.8,
+ comment: "Changing quantity from 10 to 6.8"
+ }
+ });
+
+ const { mutate } = makeClient(parentAppendixUser);
+ const { data, errors } = await mutate<
+ Pick,
+ MutationSubmitFormRevisionRequestApprovalArgs
+ >(SUBMIT_BSDD_REVISION_REQUEST_APPROVAL, {
+ variables: {
+ id: revisionRequest.id,
+ isApproved: true
+ }
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.submitFormRevisionRequestApproval.status).toBe(
+ RevisionRequestStatus.ACCEPTED
+ );
+
+ const updatedAppendix1Child = await prisma.form.findUniqueOrThrow({
+ where: { id: appendix1Child.id }
+ });
+ expect(updatedAppendix1Child.wasteDetailsQuantity?.toNumber()).toEqual(6.8);
+
+ const updatedAppendix1Parent = await prisma.form.findUniqueOrThrow({
+ where: { id: appendix1Parent.id }
+ });
+ expect(updatedAppendix1Parent.wasteDetailsQuantity?.toNumber()).toEqual(
+ 6.8
+ );
+ });
+
+ it("should udate appendix 1 parent's quantity if revision modified child's quantity (multiple children)", async () => {
+ // Given
+ const { company: childAppendixEmitter } = await userWithCompanyFactory(
+ "ADMIN"
+ );
+ const { user: parentAppendixUser, company: parentAppendixEmitter } =
+ await userWithCompanyFactory("ADMIN");
+ const { company: transporter } = await userWithCompanyFactory("ADMIN");
+
+ const appendix1Child1 = await prisma.form.create({
+ data: {
+ readableId: getReadableId(),
+ status: Status.RECEIVED,
+ emitterType: EmitterType.APPENDIX1_PRODUCER,
+ emitterCompanySiret: childAppendixEmitter.siret,
+ wasteDetailsCode: "15 01 10*",
+ wasteDetailsQuantity: 10,
+ owner: { connect: { id: parentAppendixUser.id } },
+ transporters: {
+ create: {
+ number: 1,
+ transporterCompanySiret: transporter.siret
+ }
+ }
+ }
+ });
+
+ const appendix1Child2 = await prisma.form.create({
+ data: {
+ readableId: getReadableId(),
+ status: Status.RECEIVED,
+ emitterType: EmitterType.APPENDIX1_PRODUCER,
+ emitterCompanySiret: childAppendixEmitter.siret,
+ wasteDetailsCode: "15 01 10*",
+ wasteDetailsQuantity: 5.5,
+ owner: { connect: { id: parentAppendixUser.id } },
+ transporters: {
+ create: {
+ number: 1,
+ transporterCompanySiret: transporter.siret
+ }
+ }
+ }
+ });
+
+ const appendix1Parent = await formFactory({
+ ownerId: parentAppendixUser.id,
+ opt: {
+ status: Status.SENT,
+ emitterType: EmitterType.APPENDIX1,
+ emitterCompanySiret: parentAppendixEmitter.siret,
+ emitterCompanyName: parentAppendixEmitter.name,
+ recipientCompanySiret: parentAppendixEmitter.siret,
+ wasteDetailsQuantity: 15.5,
+ grouping: {
+ create: [
+ { initialFormId: appendix1Child1.id, quantity: 0 },
+ { initialFormId: appendix1Child2.id, quantity: 0 }
+ ]
+ },
+ transporters: {
+ create: {
+ number: 1,
+ transporterCompanySiret: transporter.siret
+ }
+ }
+ }
+ });
+
+ // When
+ const revisionRequest = await prisma.bsddRevisionRequest.create({
+ data: {
+ bsddId: appendix1Child1.id,
+ authoringCompanyId: childAppendixEmitter.id,
+ approvals: { create: { approverSiret: parentAppendixEmitter.siret! } },
+ wasteDetailsQuantity: 6.8,
+ comment: "Changing quantity from 10 to 6.8"
+ }
+ });
+
+ const { mutate } = makeClient(parentAppendixUser);
+ const { data, errors } = await mutate<
+ Pick,
+ MutationSubmitFormRevisionRequestApprovalArgs
+ >(SUBMIT_BSDD_REVISION_REQUEST_APPROVAL, {
+ variables: {
+ id: revisionRequest.id,
+ isApproved: true
+ }
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.submitFormRevisionRequestApproval.status).toBe(
+ RevisionRequestStatus.ACCEPTED
+ );
+
+ const updatedAppendix1Child = await prisma.form.findUniqueOrThrow({
+ where: { id: appendix1Child1.id }
+ });
+ expect(updatedAppendix1Child.wasteDetailsQuantity?.toNumber()).toBe(6.8);
+
+ const updatedAppendix1Parent = await prisma.form.findUniqueOrThrow({
+ where: { id: appendix1Parent.id }
+ });
+ expect(updatedAppendix1Parent.wasteDetailsQuantity?.toNumber()).toBe(12.3);
+ });
+
+ it("should udate bsdd sample number", async () => {
+ // Given
+ const { user, company: emitterCompany } = await userWithCompanyFactory(
+ "ADMIN"
+ );
+ const { user: transporter, company: transporterCompany } =
+ await userWithCompanyFactory("ADMIN");
+
+ const bsdd = await prisma.form.create({
+ data: {
+ readableId: getReadableId(),
+ status: Status.RECEIVED,
+ emitterCompanySiret: emitterCompany.siret,
+ wasteDetailsCode: "15 01 10*",
+ wasteDetailsSampleNumber: "sample number 1",
+ owner: { connect: { id: user.id } },
+ transporters: {
+ create: {
+ number: 1,
+ transporterCompanySiret: transporterCompany.siret
+ }
+ }
+ }
+ });
+
+ // When
+ const revisionRequest = await prisma.bsddRevisionRequest.create({
+ data: {
+ bsddId: bsdd.id,
+ authoringCompanyId: emitterCompany.id,
+ approvals: { create: { approverSiret: transporterCompany.siret! } },
+ wasteDetailsSampleNumber: "sample number 2",
+ comment: "Changing sample number from 1 to 2"
+ }
+ });
+
+ const { mutate } = makeClient(transporter);
+ const { data, errors } = await mutate<
+ Pick,
+ MutationSubmitFormRevisionRequestApprovalArgs
+ >(SUBMIT_BSDD_REVISION_REQUEST_APPROVAL, {
+ variables: {
+ id: revisionRequest.id,
+ isApproved: true
+ }
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.submitFormRevisionRequestApproval.status).toBe(
+ RevisionRequestStatus.ACCEPTED
+ );
+
+ const updatedBsdd = await prisma.form.findUniqueOrThrow({
+ where: { id: bsdd.id }
+ });
+ expect(updatedBsdd.wasteDetailsSampleNumber).toEqual("sample number 2");
+ });
+
+ it("should udate bsdd wasteDetailsQuantity", async () => {
+ // Given
+ const { user, company: emitterCompany } = await userWithCompanyFactory(
+ "ADMIN"
+ );
+ const { user: transporter, company: transporterCompany } =
+ await userWithCompanyFactory("ADMIN");
+
+ const bsdd = await prisma.form.create({
+ data: {
+ readableId: getReadableId(),
+ status: Status.RECEIVED,
+ emitterCompanySiret: emitterCompany.siret,
+ wasteDetailsCode: "15 01 10*",
+ wasteDetailsQuantity: 10,
+ owner: { connect: { id: user.id } },
+ transporters: {
+ create: {
+ number: 1,
+ transporterCompanySiret: transporterCompany.siret
+ }
+ }
+ }
+ });
+
+ // When
+ const revisionRequest = await prisma.bsddRevisionRequest.create({
+ data: {
+ bsddId: bsdd.id,
+ authoringCompanyId: emitterCompany.id,
+ approvals: { create: { approverSiret: transporterCompany.siret! } },
+ wasteDetailsQuantity: 6.5,
+ comment: "Changing wasteDetailsQuantity from 10 to 6.5"
+ }
+ });
+
+ const { mutate } = makeClient(transporter);
+ const { data, errors } = await mutate<
+ Pick,
+ MutationSubmitFormRevisionRequestApprovalArgs
+ >(SUBMIT_BSDD_REVISION_REQUEST_APPROVAL, {
+ variables: {
+ id: revisionRequest.id,
+ isApproved: true
+ }
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.submitFormRevisionRequestApproval.status).toBe(
+ RevisionRequestStatus.ACCEPTED
+ );
+
+ const updatedBsdd = await prisma.form.findUniqueOrThrow({
+ where: { id: bsdd.id }
+ });
+ expect(updatedBsdd.wasteDetailsQuantity?.toNumber()).toEqual(6.5);
+ });
+
it("should change the operation code & mode", async () => {
const { company: companyOfSomeoneElse } = await userWithCompanyFactory(
"ADMIN"
diff --git a/back/src/forms/resolvers/mutations/markAsAccepted.ts b/back/src/forms/resolvers/mutations/markAsAccepted.ts
index 9a475516f9..1aa2d898f5 100644
--- a/back/src/forms/resolvers/mutations/markAsAccepted.ts
+++ b/back/src/forms/resolvers/mutations/markAsAccepted.ts
@@ -90,7 +90,7 @@ const markAsAcceptedResolver: MutationResolvers["markAsAccepted"] = async (
if (isWasteRefused(acceptedForm)) {
const refusedEmail = await renderFormRefusedEmail(acceptedForm);
- if (refusedEmail) {
+ if (refusedEmail && refusedEmail.to?.length) {
sendMail(refusedEmail);
}
}
diff --git a/back/src/forms/resolvers/mutations/markAsSealed.ts b/back/src/forms/resolvers/mutations/markAsSealed.ts
index be04f98f2d..39fe452681 100644
--- a/back/src/forms/resolvers/mutations/markAsSealed.ts
+++ b/back/src/forms/resolvers/mutations/markAsSealed.ts
@@ -1,5 +1,5 @@
import { EmitterType, Form, Status } from "@prisma/client";
-import { contentAwaitsGuest, renderMail } from "@td/mail";
+import { yourCompanyIsIdentifiedOnABsd, renderMail } from "@td/mail";
import { prisma } from "@td/prisma";
import { UserInputError } from "../../../common/errors";
import { checkIsAuthenticated } from "../../../common/permissions";
@@ -141,24 +141,36 @@ async function mailToNonExistentEmitter(
if (
form.emitterCompanyMail &&
form.emitterCompanySiret &&
+ form.emitterCompanyName &&
+ form.recipientCompanyName &&
+ form.recipientCompanySiret &&
!contactAlreadyMentionned
) {
- await sendMail(
- renderMail(contentAwaitsGuest, {
- to: [
- {
- email: form.emitterCompanyMail,
- name: form.emitterCompanyContact ?? ""
- }
- ],
- variables: {
- company: {
- siret: form.emitterCompanySiret,
- name: form.emitterCompanyName ?? ""
- }
+ const mail = renderMail(yourCompanyIsIdentifiedOnABsd, {
+ to: [
+ {
+ email: form.emitterCompanyMail,
+ name: form.emitterCompanyContact ?? ""
}
- })
- );
+ ],
+ variables: {
+ emitter: {
+ siret: form.emitterCompanySiret,
+ name: form.emitterCompanyName
+ },
+ destination: {
+ siret: form.recipientCompanySiret,
+ name: form.recipientCompanyName
+ }
+ }
+ });
+
+ await sendMail({
+ ...mail,
+ // permet de cacher le message "Vous avez reçu cet e-mail car vous
+ // êtes inscrit sur la plateforme Trackdéchets" dans le template Brevo
+ params: { hideRegisteredUserInfo: true }
+ });
}
}
diff --git a/back/src/forms/resolvers/queries/__tests__/forms.integration.ts b/back/src/forms/resolvers/queries/__tests__/forms.integration.ts
index cc7140e547..72f058d1be 100644
--- a/back/src/forms/resolvers/queries/__tests__/forms.integration.ts
+++ b/back/src/forms/resolvers/queries/__tests__/forms.integration.ts
@@ -888,13 +888,14 @@ describe("Integration / Forms query for transporters", () => {
// the transporter makes the query
const { query } = makeClient(user);
- const { data } = await query>(FORMS, {
+ const { data, errors } = await query>(FORMS, {
variables: {
siret: transporter.siret,
roles: ["TRANSPORTER"]
}
});
+ expect(errors).toBeUndefined();
expect(data.forms.length).toBe(1);
expect(data.forms[0].id).toBe(form.id);
});
diff --git a/back/src/forms/sirenify.ts b/back/src/forms/sirenify.ts
index 92d98d0f48..c62846e064 100644
--- a/back/src/forms/sirenify.ts
+++ b/back/src/forms/sirenify.ts
@@ -1,5 +1,8 @@
import { Prisma } from "@prisma/client";
-import buildSirenify, { nextBuildSirenify } from "../companies/sirenify";
+import buildSirenify, {
+ nextBuildSirenify,
+ NextCompanyInputAccessor
+} from "../companies/sirenify";
import {
CompanyInput,
CreateFormInput,
@@ -127,7 +130,7 @@ export const sirenifyTransporterInput = buildSirenify(
const formCreateInputAccessors = (
formCreateInput: Prisma.FormCreateInput,
sealedFields: string[] = [] // Tranformations should not be run on sealed fields
-) => [
+): NextCompanyInputAccessor[] => [
{
siret: formCreateInput?.emitterCompanySiret,
skip: sealedFields.includes("emitterCompanySiret"),
@@ -164,7 +167,9 @@ const formCreateInputAccessors = (
siret: formCreateInput?.ecoOrganismeSiret,
skip: sealedFields.includes("ecoOrganismeSiret"),
setter: (formCreateInput, companyInput: CompanyInput) => {
- formCreateInput.ecoOrganismeName = companyInput.name;
+ if (companyInput.name) {
+ formCreateInput.ecoOrganismeName = companyInput.name;
+ }
}
},
...(
@@ -194,10 +199,14 @@ const formCreateInputAccessors = (
)?.transporterCompanySiret ||
sealedFields.includes("transporterCompanySiret"),
setter: (formCreateInput, companyInput: CompanyInput) => {
- formCreateInput.transporters.create.transporterCompanyName =
- companyInput.name;
- formCreateInput.transporters.create.transporterCompanyAddress =
- companyInput.address;
+ (
+ formCreateInput.transporters
+ ?.create as Prisma.BsddTransporterCreateWithoutFormInput
+ ).transporterCompanyName = companyInput.name;
+ (
+ formCreateInput.transporters
+ ?.create as Prisma.BsddTransporterCreateWithoutFormInput
+ ).transporterCompanyAddress = companyInput.address;
}
}
];
diff --git a/back/src/forms/updateAppendix2.ts b/back/src/forms/updateAppendix2.ts
index 6d8362b01b..17862fcaa5 100644
--- a/back/src/forms/updateAppendix2.ts
+++ b/back/src/forms/updateAppendix2.ts
@@ -3,6 +3,7 @@ import { EmitterType, Status } from "@prisma/client";
import Decimal from "decimal.js";
import { enqueueUpdateAppendix2Job } from "../queue/producers/updateAppendix2";
import { getFormRepository } from "./repository";
+import { bsddWasteQuantities } from "./helpers/bsddWasteQuantities";
const DECIMAL_WEIGHT_PRECISION = 6; // gramme
@@ -55,10 +56,15 @@ export async function updateAppendix2Fn(args: UpdateAppendix2FnArgs) {
.filter(grp => grp.initialFormId === form.id)
.map(g => g.nextForm);
+ const wasteQuantities = bsddWasteQuantities(form.forwardedIn ?? form);
+
const quantityReceived = form.forwardedIn
? form.forwardedIn.quantityReceived
: form.quantityReceived;
+ const quantityAccepted =
+ wasteQuantities?.quantityAccepted ?? quantityReceived;
+
const quantityGrouped = new Decimal(
groupements
.filter(grp => grp.initialFormId === form.id)
@@ -68,9 +74,9 @@ export async function updateAppendix2Fn(args: UpdateAppendix2FnArgs) {
// on a quelques quantityReceived avec des décimales au delà du gramme
const groupedInTotality =
- quantityReceived &&
+ quantityAccepted &&
quantityGrouped.greaterThanOrEqualTo(
- new Decimal(quantityReceived).toDecimalPlaces(DECIMAL_WEIGHT_PRECISION) // limit precision to circumvent rogue decimal digits
+ quantityAccepted.toDecimalPlaces(DECIMAL_WEIGHT_PRECISION) // limit precision to circumvent rogue decimal digits
); // case > should not happen
const allSealed =
@@ -103,19 +109,26 @@ export async function updateAppendix2Fn(args: UpdateAppendix2FnArgs) {
const { update: updateForm, findGroupedFormsById } = getFormRepository(user);
- await updateForm(
- { id: form.id },
- { quantityGrouped: quantityGrouped.toNumber(), status: nextStatus }
- );
-
- if (form.emitterType === EmitterType.APPENDIX2) {
- const groupedForms = await findGroupedFormsById(form.id);
- for (const formId of groupedForms.map(f => f.id)) {
- await enqueueUpdateAppendix2Job({
- formId,
- userId: user.id,
- auth: user.auth
- });
+ if (
+ nextStatus !== form.status ||
+ !quantityGrouped.equals(new Decimal(form.quantityGrouped))
+ ) {
+ await updateForm(
+ { id: form.id },
+ { quantityGrouped: quantityGrouped.toNumber(), status: nextStatus }
+ );
+ if (
+ form.emitterType === EmitterType.APPENDIX2 &&
+ nextStatus === "PROCESSED"
+ ) {
+ const groupedForms = await findGroupedFormsById(form.id);
+ for (const formId of groupedForms.map(f => f.id)) {
+ await enqueueUpdateAppendix2Job({
+ formId,
+ userId: user.id,
+ auth: user.auth
+ });
+ }
}
}
}
diff --git a/back/src/index.ts b/back/src/index.ts
index 990e04194e..c55320f0d9 100644
--- a/back/src/index.ts
+++ b/back/src/index.ts
@@ -16,7 +16,7 @@ export { closeQueues } from "./queue/producers";
export { initSentry } from "./common/sentry";
export * from "./utils";
export { deleteBsd } from "./common/elastic";
-export { getCompaniesAndActiveAdminsByCompanyOrgIds } from "./companies/database";
+export { getCompaniesAndSubscribersByCompanyOrgIds } from "./companies/database";
export { formatDate } from "./common/pdf";
export { sendMail } from "./mailer/mailing";
export { BsdUpdateQueueItem, updatesQueue } from "./queue/producers/bsdUpdate";
@@ -82,3 +82,4 @@ export { gericoQueue } from "./queue/producers/gerico";
export { getBsdasriFromActivityEvents } from "./activity-events/bsdasri";
export { getBsdaFromActivityEvents } from "./activity-events/bsda";
export { getBsddFromActivityEvents } from "./activity-events/bsdd";
+export { cleanUpIsReturnForTab } from "./common/elasticHelpers";
diff --git a/back/src/mailer/mailing.ts b/back/src/mailer/mailing.ts
index e5b6c76177..1299ac6c6a 100644
--- a/back/src/mailer/mailing.ts
+++ b/back/src/mailer/mailing.ts
@@ -2,8 +2,16 @@ import { backend, Mail, Contact } from "@td/mail";
import { addToMailQueue } from "../queue/producers/mail";
import { logger } from "@td/logger";
+type SendMailOpts = {
+ sync: boolean;
+};
+
// push job to the job queue for the api server not to execute the sendMail itself
-export async function sendMail(mail: Mail): Promise {
+export async function sendMail(mail: Mail, opts?: SendMailOpts): Promise {
+ if (opts?.sync) {
+ await sendMailSync(mail);
+ return;
+ }
try {
await addToMailQueue(mail);
} catch (err) {
diff --git a/back/src/permissions/permissions.ts b/back/src/permissions/permissions.ts
index c88f82cc16..5ef8e63646 100644
--- a/back/src/permissions/permissions.ts
+++ b/back/src/permissions/permissions.ts
@@ -33,7 +33,8 @@ export enum Permission {
CompanyCanVerify = "CompanyCanVerify",
CompanyCanManageSignatureAutomation = "CompanyCanManageSignatureAutomation",
CompanyCanManageMembers = "CompanyCanManageMembers",
- CompanyCanRenewSecurityCode = "CompanyCanRenewSecurityCode"
+ CompanyCanRenewSecurityCode = "CompanyCanRenewSecurityCode",
+ CompanyCanManageRegistryDelegation = "CompanyCanManageRegistryDelegation"
}
export function toGraphQLPermission(permission: Permission): UserPermission {
@@ -57,7 +58,9 @@ export function toGraphQLPermission(permission: Permission): UserPermission {
[Permission.CompanyCanManageSignatureAutomation]:
"COMPANY_CAN_MANAGE_SIGNATURE_AUTOMATION",
[Permission.CompanyCanManageMembers]: "COMPANY_CAN_MANAGE_MEMBERS",
- [Permission.CompanyCanRenewSecurityCode]: "COMPANY_CAN_RENEW_SECURITY_CODE"
+ [Permission.CompanyCanRenewSecurityCode]: "COMPANY_CAN_RENEW_SECURITY_CODE",
+ [Permission.CompanyCanManageRegistryDelegation]:
+ "COMPANY_CAN_MANAGE_REGISTRY_DELEAGATION"
};
return mapping[permission];
}
@@ -105,7 +108,8 @@ const adminPermissions = [
Permission.CompanyCanVerify,
Permission.CompanyCanManageSignatureAutomation,
Permission.CompanyCanManageMembers,
- Permission.CompanyCanRenewSecurityCode
+ Permission.CompanyCanRenewSecurityCode,
+ Permission.CompanyCanManageRegistryDelegation
];
export const grants: { [Key in UserRole]: Permission[] } = {
diff --git a/back/src/registry/__tests__/streams.integration.ts b/back/src/registry/__tests__/streams.integration.ts
index 645390a83e..009bdd6b44 100644
--- a/back/src/registry/__tests__/streams.integration.ts
+++ b/back/src/registry/__tests__/streams.integration.ts
@@ -111,6 +111,7 @@ describe("wastesReader", () => {
.fill(1)
.map(() =>
bsvhuFactory({
+ userId: destination.user.id,
opt: {
destinationCompanySiret: destination.company.siret,
destinationReceptionDate: new Date(),
diff --git a/back/src/registryDelegation/__tests__/factories.ts b/back/src/registryDelegation/__tests__/factories.ts
new file mode 100644
index 0000000000..b3c2d0e7e6
--- /dev/null
+++ b/back/src/registryDelegation/__tests__/factories.ts
@@ -0,0 +1,47 @@
+import { Company, Prisma } from "@prisma/client";
+import { prisma } from "@td/prisma";
+import { userWithCompanyFactory } from "../../__tests__/factories";
+import { startOfDay } from "../../utils";
+
+export const registryDelegationFactory = async (
+ opt?: Partial
+) => {
+ const { user: delegateUser, company: delegateCompany } =
+ await userWithCompanyFactory();
+ const { user: delegatorUser, company: delegatorCompany } =
+ await userWithCompanyFactory();
+
+ const delegation = await prisma.registryDelegation.create({
+ data: {
+ startDate: startOfDay(new Date()),
+ delegate: { connect: { id: delegateCompany.id } },
+ delegator: { connect: { id: delegatorCompany.id } },
+ ...opt
+ }
+ });
+
+ return {
+ delegation,
+ delegateUser,
+ delegateCompany,
+ delegatorUser,
+ delegatorCompany
+ };
+};
+
+export const registryDelegationFactoryWithExistingCompanies = async (
+ delegate: Company,
+ delegator: Company,
+ opt?: Partial
+) => {
+ const delegation = await prisma.registryDelegation.create({
+ data: {
+ startDate: startOfDay(new Date()),
+ delegate: { connect: { id: delegate.id } },
+ delegator: { connect: { id: delegator.id } },
+ ...opt
+ }
+ });
+
+ return delegation;
+};
diff --git a/back/src/registryDelegation/permissions.ts b/back/src/registryDelegation/permissions.ts
new file mode 100644
index 0000000000..4be7119354
--- /dev/null
+++ b/back/src/registryDelegation/permissions.ts
@@ -0,0 +1,140 @@
+import { Company, RegistryDelegation, User } from "@prisma/client";
+import { prisma } from "@td/prisma";
+import { ForbiddenError } from "../common/errors";
+import { can, Permission } from "../permissions";
+
+export async function checkCanCreate(user: User, delegator: Company) {
+ const companyAssociation = await prisma.companyAssociation.findFirst({
+ where: {
+ userId: user.id,
+ companyId: delegator.id
+ },
+ select: {
+ role: true
+ }
+ });
+
+ if (!companyAssociation) {
+ throw new ForbiddenError(
+ "Vous devez faire partie de l'entreprise délégante pour pouvoir créer une délégation."
+ );
+ }
+
+ if (
+ !can(companyAssociation.role, Permission.CompanyCanManageRegistryDelegation)
+ ) {
+ throw new ForbiddenError(
+ "Vous n'avez pas les permissions suffisantes pour pouvoir créer une délégation."
+ );
+ }
+}
+
+export async function checkCanAccess(
+ user: User,
+ delegation: RegistryDelegation
+) {
+ const companyAssociation = await prisma.companyAssociation.findFirst({
+ where: {
+ userId: user.id,
+ companyId: { in: [delegation.delegateId, delegation.delegatorId] }
+ },
+ select: {
+ id: true
+ }
+ });
+
+ if (!companyAssociation) {
+ throw new ForbiddenError(
+ "Vous devez faire partie de l'entreprise délégante ou délégataire d'une délégation pour y avoir accès."
+ );
+ }
+}
+
+export async function checkCanRevoke(
+ user: User,
+ delegation: RegistryDelegation
+) {
+ if (delegation.revokedBy) {
+ throw new ForbiddenError("Cette délégation a déjà été révoquée.");
+ }
+
+ if (delegation.cancelledBy) {
+ throw new ForbiddenError("Cette délégation a été annulée.");
+ }
+
+ const companyAssociation = await prisma.companyAssociation.findFirst({
+ where: {
+ userId: user.id,
+ companyId: delegation.delegatorId
+ },
+ select: {
+ role: true
+ }
+ });
+
+ if (!companyAssociation) {
+ throw new ForbiddenError(
+ "Vous devez faire partie de l'entreprise délégante d'une délégation pour pouvoir la révoquer."
+ );
+ }
+
+ if (
+ !can(companyAssociation.role, Permission.CompanyCanManageRegistryDelegation)
+ ) {
+ throw new ForbiddenError(
+ "Vous n'avez pas les permissions suffisantes pour pouvoir révoquer une délégation."
+ );
+ }
+}
+
+export async function checkCanCancel(
+ user: User,
+ delegation: RegistryDelegation
+) {
+ if (delegation.cancelledBy) {
+ throw new ForbiddenError("Cette délégation a déjà été annulée.");
+ }
+
+ if (delegation.revokedBy) {
+ throw new ForbiddenError("Cette délégation a été révoquée.");
+ }
+
+ const companyAssociation = await prisma.companyAssociation.findFirst({
+ where: {
+ userId: user.id,
+ companyId: delegation.delegateId
+ },
+ select: {
+ role: true
+ }
+ });
+
+ if (!companyAssociation) {
+ throw new ForbiddenError(
+ "Vous devez faire partie de l'entreprise délégataire d'une délégation pour pouvoir l'annuler."
+ );
+ }
+
+ if (
+ !can(companyAssociation.role, Permission.CompanyCanManageRegistryDelegation)
+ ) {
+ throw new ForbiddenError(
+ "Vous n'avez pas les permissions suffisantes pour pouvoir annuler une délégation."
+ );
+ }
+}
+
+export async function checkBelongsTo(user: User, company: Company) {
+ const companyAssociation = await prisma.companyAssociation.findFirst({
+ where: {
+ userId: user.id,
+ companyId: company.id
+ }
+ });
+
+ if (!companyAssociation) {
+ throw new ForbiddenError(
+ `L'utilisateur ne fait pas partie de l'entreprise ${company.orgId}.`
+ );
+ }
+}
diff --git a/back/src/registryDelegation/repository/index.ts b/back/src/registryDelegation/repository/index.ts
new file mode 100644
index 0000000000..8aeb71a92d
--- /dev/null
+++ b/back/src/registryDelegation/repository/index.ts
@@ -0,0 +1,37 @@
+import { prisma } from "@td/prisma";
+import { buildCreateRegistryDelegation } from "./registryDelegation/create";
+import { transactionWrapper } from "../../common/repository/helper";
+import {
+ RepositoryFnBuilder,
+ RepositoryTransaction
+} from "../../common/repository/types";
+import { RegistryDelegationActions } from "./types";
+import buildFindFirstRegistryDelegation from "./registryDelegation/findFirst";
+import { buildUpdateRegistryDelegation } from "./registryDelegation/update";
+import { buildCountRegistryDelegations } from "./registryDelegation/count";
+import { buildFindManyRegistryDelegation } from "./registryDelegation/findMany";
+
+export type RegistryDelegationRepository = RegistryDelegationActions;
+
+export function getReadonlyRegistryDelegationRepository() {
+ return {
+ findFirst: buildFindFirstRegistryDelegation({ prisma }),
+ count: buildCountRegistryDelegations({ prisma }),
+ findMany: buildFindManyRegistryDelegation({ prisma })
+ };
+}
+
+export function getRegistryDelegationRepository(
+ user: Express.User,
+ transaction?: RepositoryTransaction
+): RegistryDelegationRepository {
+ function useTransaction(builder: RepositoryFnBuilder) {
+ return transactionWrapper(builder, { user, transaction });
+ }
+
+ return {
+ ...getReadonlyRegistryDelegationRepository(),
+ create: useTransaction(buildCreateRegistryDelegation),
+ update: useTransaction(buildUpdateRegistryDelegation)
+ };
+}
diff --git a/back/src/registryDelegation/repository/registryDelegation/count.ts b/back/src/registryDelegation/repository/registryDelegation/count.ts
new file mode 100644
index 0000000000..b9141e169d
--- /dev/null
+++ b/back/src/registryDelegation/repository/registryDelegation/count.ts
@@ -0,0 +1,14 @@
+import { Prisma } from "@prisma/client";
+import { ReadRepositoryFnDeps } from "../../../common/repository/types";
+
+export type CountRegistryDelegationsFn = (
+ where: Prisma.RegistryDelegationWhereInput
+) => Promise;
+
+export function buildCountRegistryDelegations({
+ prisma
+}: ReadRepositoryFnDeps): CountRegistryDelegationsFn {
+ return where => {
+ return prisma.registryDelegation.count({ where });
+ };
+}
diff --git a/back/src/registryDelegation/repository/registryDelegation/create.ts b/back/src/registryDelegation/repository/registryDelegation/create.ts
new file mode 100644
index 0000000000..3aebb5afda
--- /dev/null
+++ b/back/src/registryDelegation/repository/registryDelegation/create.ts
@@ -0,0 +1,34 @@
+import { Prisma, RegistryDelegation } from "@prisma/client";
+import {
+ LogMetadata,
+ RepositoryFnDeps
+} from "../../../common/repository/types";
+
+export type CreateRegistryDelegationFn = (
+ data: Prisma.RegistryDelegationCreateInput,
+ logMetadata?: LogMetadata
+) => Promise;
+
+export const buildCreateRegistryDelegation = (
+ deps: RepositoryFnDeps
+): CreateRegistryDelegationFn => {
+ return async (data, logMetadata?) => {
+ const { prisma, user } = deps;
+
+ const delegation = await prisma.registryDelegation.create({
+ data
+ });
+
+ await prisma.event.create({
+ data: {
+ streamId: delegation.id,
+ actor: user.id,
+ type: "RegistryDelegationCreated",
+ data: { content: data } as Prisma.InputJsonObject,
+ metadata: { ...logMetadata, authType: user.auth }
+ }
+ });
+
+ return delegation;
+ };
+};
diff --git a/back/src/registryDelegation/repository/registryDelegation/findFirst.ts b/back/src/registryDelegation/repository/registryDelegation/findFirst.ts
new file mode 100644
index 0000000000..cdd3a55b3d
--- /dev/null
+++ b/back/src/registryDelegation/repository/registryDelegation/findFirst.ts
@@ -0,0 +1,18 @@
+import { Prisma, RegistryDelegation } from "@prisma/client";
+import { ReadRepositoryFnDeps } from "../../../common/repository/types";
+
+export type FindFirstRegistryDelegationFn = (
+ where: Prisma.RegistryDelegationWhereInput,
+ options?: Omit
+) => Promise;
+
+const buildFindFirstRegistryDelegation: (
+ deps: ReadRepositoryFnDeps
+) => FindFirstRegistryDelegationFn =
+ ({ prisma }) =>
+ (where, options?) => {
+ const input = { where, ...options };
+ return prisma.registryDelegation.findFirst(input);
+ };
+
+export default buildFindFirstRegistryDelegation;
diff --git a/back/src/registryDelegation/repository/registryDelegation/findMany.ts b/back/src/registryDelegation/repository/registryDelegation/findMany.ts
new file mode 100644
index 0000000000..5b17bddf6b
--- /dev/null
+++ b/back/src/registryDelegation/repository/registryDelegation/findMany.ts
@@ -0,0 +1,26 @@
+import { Prisma } from "@prisma/client";
+import { ReadRepositoryFnDeps } from "../../../common/repository/types";
+
+export type FindManyRegistryDelegationFn = <
+ Args extends Omit
+>(
+ where: Prisma.RegistryDelegationWhereInput,
+ options?: Args
+) => Promise>>;
+
+export function buildFindManyRegistryDelegation({
+ prisma
+}: ReadRepositoryFnDeps): FindManyRegistryDelegationFn {
+ return async <
+ Args extends Omit
+ >(
+ where: Prisma.RegistryDelegationWhereInput,
+ options?: Args
+ ) => {
+ const input = { where, ...options };
+ const registryDelegations = await prisma.registryDelegation.findMany(input);
+ return registryDelegations as Array<
+ Prisma.RegistryDelegationGetPayload
+ >;
+ };
+}
diff --git a/back/src/registryDelegation/repository/registryDelegation/update.ts b/back/src/registryDelegation/repository/registryDelegation/update.ts
new file mode 100644
index 0000000000..a8a8755099
--- /dev/null
+++ b/back/src/registryDelegation/repository/registryDelegation/update.ts
@@ -0,0 +1,36 @@
+import { Prisma, RegistryDelegation } from "@prisma/client";
+import {
+ LogMetadata,
+ RepositoryFnDeps
+} from "../../../common/repository/types";
+
+export type UpdateRegistryDelegationFn = (
+ where: Prisma.RegistryDelegationWhereUniqueInput,
+ data: Prisma.RegistryDelegationUpdateInput,
+ logMetadata?: LogMetadata
+) => Promise;
+
+export const buildUpdateRegistryDelegation = (
+ deps: RepositoryFnDeps
+): UpdateRegistryDelegationFn => {
+ return async (where, data, logMetadata) => {
+ const { prisma, user } = deps;
+
+ const delegation = await prisma.registryDelegation.update({
+ where,
+ data
+ });
+
+ await prisma.event.create({
+ data: {
+ streamId: delegation.id,
+ actor: user.id,
+ type: "RegistryDelegationUpdated",
+ data: { content: data } as Prisma.InputJsonObject,
+ metadata: { ...logMetadata, authType: user.auth }
+ }
+ });
+
+ return delegation;
+ };
+};
diff --git a/back/src/registryDelegation/repository/types.ts b/back/src/registryDelegation/repository/types.ts
new file mode 100644
index 0000000000..5470b33eea
--- /dev/null
+++ b/back/src/registryDelegation/repository/types.ts
@@ -0,0 +1,16 @@
+import { CountRegistryDelegationsFn } from "./registryDelegation/count";
+import { CreateRegistryDelegationFn } from "./registryDelegation/create";
+import { FindFirstRegistryDelegationFn } from "./registryDelegation/findFirst";
+import { FindManyRegistryDelegationFn } from "./registryDelegation/findMany";
+import { UpdateRegistryDelegationFn } from "./registryDelegation/update";
+
+export type RegistryDelegationActions = {
+ // Read
+ findFirst: FindFirstRegistryDelegationFn;
+ count: CountRegistryDelegationsFn;
+ findMany: FindManyRegistryDelegationFn;
+
+ // Write
+ create: CreateRegistryDelegationFn;
+ update: UpdateRegistryDelegationFn;
+};
diff --git a/back/src/registryDelegation/resolvers/Mutation.ts b/back/src/registryDelegation/resolvers/Mutation.ts
new file mode 100644
index 0000000000..6cc39114dc
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/Mutation.ts
@@ -0,0 +1,19 @@
+import { MutationResolvers } from "../../generated/graphql/types";
+import cancelRegistryDelegation from "./mutations/cancelRegistryDelegation";
+import createRegistryDelegation from "./mutations/createRegistryDelegation";
+import revokeRegistryDelegation from "./mutations/revokeRegistryDelegation";
+
+export type RegistryDelegationMutationResolvers = Pick<
+ MutationResolvers,
+ | "createRegistryDelegation"
+ | "revokeRegistryDelegation"
+ | "cancelRegistryDelegation"
+>;
+
+const Mutation: RegistryDelegationMutationResolvers = {
+ createRegistryDelegation,
+ revokeRegistryDelegation,
+ cancelRegistryDelegation
+};
+
+export default Mutation;
diff --git a/back/src/registryDelegation/resolvers/Query.ts b/back/src/registryDelegation/resolvers/Query.ts
new file mode 100644
index 0000000000..2b72dc96b3
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/Query.ts
@@ -0,0 +1,15 @@
+import { QueryResolvers } from "../../generated/graphql/types";
+import registryDelegation from "./queries/registryDelegation";
+import registryDelegations from "./queries/registryDelegations";
+
+export type RegistryDelegationQueryResolvers = Pick<
+ QueryResolvers,
+ "registryDelegation" | "registryDelegations"
+>;
+
+const Query: RegistryDelegationQueryResolvers = {
+ registryDelegation,
+ registryDelegations
+};
+
+export default Query;
diff --git a/back/src/registryDelegation/resolvers/__tests__/status.integration.ts b/back/src/registryDelegation/resolvers/__tests__/status.integration.ts
new file mode 100644
index 0000000000..92f0a970aa
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/__tests__/status.integration.ts
@@ -0,0 +1,98 @@
+import { Prisma } from "@prisma/client";
+import { nowPlusXHours } from "../../../utils";
+import { registryDelegationFactory } from "../../__tests__/factories";
+import { getDelegation } from "../queries/__tests__/registryDelegation.integration";
+import { RegistryDelegationStatus } from "../../../generated/graphql/types";
+
+describe("status", () => {
+ it.each([
+ // REVOKED
+ [
+ { revokedBy: "someuserid", startDate: nowPlusXHours(-2) },
+ "REVOKED" as RegistryDelegationStatus
+ ],
+ [
+ { revokedBy: "someuserid", startDate: nowPlusXHours(2) },
+ "REVOKED" as RegistryDelegationStatus
+ ],
+ [
+ {
+ revokedBy: "someuserid",
+ startDate: nowPlusXHours(2),
+ endDate: nowPlusXHours(3)
+ },
+ "REVOKED" as RegistryDelegationStatus
+ ],
+ [
+ {
+ revokedBy: "someuserid",
+ startDate: nowPlusXHours(-4),
+ endDate: nowPlusXHours(-3)
+ },
+ "REVOKED" as RegistryDelegationStatus
+ ],
+ // CANCELLED
+ [
+ { cancelledBy: "someuserid", startDate: nowPlusXHours(-2) },
+ "CANCELLED" as RegistryDelegationStatus
+ ],
+ [
+ { cancelledBy: "someuserid", startDate: nowPlusXHours(2) },
+ "CANCELLED" as RegistryDelegationStatus
+ ],
+ [
+ {
+ cancelledBy: "someuserid",
+ startDate: nowPlusXHours(2),
+ endDate: nowPlusXHours(3)
+ },
+ "CANCELLED" as RegistryDelegationStatus
+ ],
+ [
+ {
+ cancelledBy: "someuserid",
+ startDate: nowPlusXHours(-4),
+ endDate: nowPlusXHours(-3)
+ },
+ "CANCELLED" as RegistryDelegationStatus
+ ],
+ // EXPIRED
+ [
+ {
+ startDate: nowPlusXHours(-4),
+ endDate: nowPlusXHours(-3)
+ },
+ "EXPIRED" as RegistryDelegationStatus
+ ],
+ // INCOMING
+ [{ startDate: nowPlusXHours(2) }, "INCOMING" as RegistryDelegationStatus],
+ [
+ { startDate: nowPlusXHours(2), endDate: nowPlusXHours(3) },
+ "INCOMING" as RegistryDelegationStatus
+ ],
+ // ONGOING
+ [{ startDate: nowPlusXHours(-1) }, "ONGOING" as RegistryDelegationStatus],
+ [
+ { startDate: nowPlusXHours(-1), endDate: nowPlusXHours(3) },
+ "ONGOING" as RegistryDelegationStatus
+ ]
+ ])(
+ "delegation = %p, should return status %p",
+ async (
+ delegationOpt: Partial,
+ status: RegistryDelegationStatus
+ ) => {
+ // Given
+ const { delegation, delegateUser } = await registryDelegationFactory(
+ delegationOpt
+ );
+
+ // When
+ const { errors, data } = await getDelegation(delegateUser, delegation.id);
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.registryDelegation.status).toBe(status);
+ }
+ );
+});
diff --git a/back/src/registryDelegation/resolvers/index.ts b/back/src/registryDelegation/resolvers/index.ts
new file mode 100644
index 0000000000..5b48652485
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/index.ts
@@ -0,0 +1,9 @@
+import Mutation from "./Mutation";
+import Query from "./Query";
+import RegistryDelegation from "./subResolvers/RegistryDelegation";
+
+export default {
+ Mutation,
+ Query,
+ RegistryDelegation
+};
diff --git a/back/src/registryDelegation/resolvers/mutations/__tests__/cancelRegistryDelegation.integration.ts b/back/src/registryDelegation/resolvers/mutations/__tests__/cancelRegistryDelegation.integration.ts
new file mode 100644
index 0000000000..803be8dfc2
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/mutations/__tests__/cancelRegistryDelegation.integration.ts
@@ -0,0 +1,209 @@
+import gql from "graphql-tag";
+import { resetDatabase } from "../../../../../integration-tests/helper";
+import { User } from "@prisma/client";
+import makeClient from "../../../../__tests__/testClient";
+import { Mutation } from "../../../../generated/graphql/types";
+import { prisma } from "@td/prisma";
+import { registryDelegationFactory } from "../../../__tests__/factories";
+import { userFactory, userInCompany } from "../../../../__tests__/factories";
+import { getStream } from "../../../../activity-events/data";
+
+const CANCEL_REGISTRY_DELEGATION = gql`
+ mutation cancelRegistryDelegation($delegationId: ID!) {
+ cancelRegistryDelegation(delegationId: $delegationId) {
+ id
+ status
+ delegate {
+ orgId
+ }
+ }
+ }
+`;
+
+const cancelDelegation = async (user: User | null, delegationId: string) => {
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate<
+ Pick
+ >(CANCEL_REGISTRY_DELEGATION, {
+ variables: {
+ delegationId
+ }
+ });
+
+ const delegation = await prisma.registryDelegation.findFirst({
+ where: { id: delegationId }
+ });
+
+ return { data, errors, delegation };
+};
+
+describe("mutation cancelRegistryDelegation", () => {
+ afterAll(resetDatabase);
+
+ describe("successful use-cases", () => {
+ it("should cancel delegation", async () => {
+ // Given
+ const { delegation, delegateUser, delegateCompany } =
+ await registryDelegationFactory();
+
+ // When
+ const {
+ errors,
+ data,
+ delegation: updatedDelegation
+ } = await cancelDelegation(delegateUser, delegation.id);
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.cancelRegistryDelegation.status).toBe("CANCELLED");
+ expect(data.cancelRegistryDelegation.delegate.orgId).toBe(
+ delegateCompany.orgId
+ );
+ expect(updatedDelegation?.cancelledBy).toBe(delegateUser.id);
+
+ // Should create an event
+ const eventsAfterCreate = await getStream(delegation!.id);
+ expect(eventsAfterCreate.length).toBe(1);
+ expect(eventsAfterCreate[0]).toMatchObject({
+ type: "RegistryDelegationUpdated",
+ actor: delegateUser.id,
+ streamId: delegation!.id
+ });
+ });
+
+ it("delegate can cancel delegation", async () => {
+ // Given
+ const { delegation, delegateUser, delegateCompany } =
+ await registryDelegationFactory();
+
+ // When
+ const {
+ errors,
+ data,
+ delegation: updatedDelegation
+ } = await cancelDelegation(delegateUser, delegation.id);
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.cancelRegistryDelegation.status).toBe("CANCELLED");
+ expect(data.cancelRegistryDelegation.delegate.orgId).toBe(
+ delegateCompany.orgId
+ );
+ expect(updatedDelegation?.cancelledBy).toBe(delegateUser.id);
+
+ // Should create an event
+ const eventsAfterCreate = await getStream(delegation!.id);
+ expect(eventsAfterCreate.length).toBe(1);
+ expect(eventsAfterCreate[0]).toMatchObject({
+ type: "RegistryDelegationUpdated",
+ actor: delegateUser.id,
+ streamId: delegation!.id
+ });
+ });
+ });
+
+ describe("authentication & roles", () => {
+ it("user must be authenticated", async () => {
+ // Given
+ const { delegation } = await registryDelegationFactory();
+
+ // When
+ const { errors } = await cancelDelegation(null, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe("Vous n'êtes pas connecté.");
+ });
+
+ it("user must belong to the delegate company", async () => {
+ // Given
+ const { delegation } = await registryDelegationFactory();
+ const user = await userFactory();
+
+ // When
+ const { errors } = await cancelDelegation(user, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "Vous devez faire partie de l'entreprise délégataire d'une délégation pour pouvoir l'annuler."
+ );
+ });
+
+ it("user must be admin", async () => {
+ // Given
+ const { delegation, delegateCompany } = await registryDelegationFactory();
+ const user = await userInCompany("MEMBER", delegateCompany.id);
+
+ // When
+ const { errors } = await cancelDelegation(user, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "Vous n'avez pas les permissions suffisantes pour pouvoir annuler une délégation."
+ );
+ });
+
+ it("delegator can NOT cancel delegation", async () => {
+ // Given
+ const { delegation, delegatorUser } = await registryDelegationFactory();
+
+ // When
+ const { errors } = await cancelDelegation(delegatorUser, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "Vous devez faire partie de l'entreprise délégataire d'une délégation pour pouvoir l'annuler."
+ );
+ });
+ });
+
+ describe("async validation", () => {
+ it("should throw if delegation does not exist", async () => {
+ // Given
+ const user = await userFactory();
+
+ // When
+ const { errors } = await cancelDelegation(
+ user,
+ "cxxxxxxxxxxxxxxxxxxxxxxxx"
+ );
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "La demande de délégation cxxxxxxxxxxxxxxxxxxxxxxxx n'existe pas."
+ );
+ });
+
+ it("cannot cancel an already cancelled delegation", async () => {
+ // Given
+ const { delegation, delegateUser } = await registryDelegationFactory({
+ cancelledBy: "someuserid"
+ });
+
+ // When
+ const { errors } = await cancelDelegation(delegateUser, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe("Cette délégation a déjà été annulée.");
+ });
+
+ it("cannot cancel a revoked delegation", async () => {
+ // Given
+ const { delegation, delegateUser } = await registryDelegationFactory({
+ revokedBy: "someuserid"
+ });
+
+ // When
+ const { errors } = await cancelDelegation(delegateUser, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe("Cette délégation a été révoquée.");
+ });
+ });
+});
diff --git a/back/src/registryDelegation/resolvers/mutations/__tests__/createRegistryDelegation.integration.ts b/back/src/registryDelegation/resolvers/mutations/__tests__/createRegistryDelegation.integration.ts
new file mode 100644
index 0000000000..241a6ceec5
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/mutations/__tests__/createRegistryDelegation.integration.ts
@@ -0,0 +1,699 @@
+import gql from "graphql-tag";
+import makeClient from "../../../../__tests__/testClient";
+import {
+ CreateRegistryDelegationInput,
+ Mutation
+} from "../../../../generated/graphql/types";
+import {
+ companyFactory,
+ userInCompany,
+ userWithCompanyFactory
+} from "../../../../__tests__/factories";
+import { User, RegistryDelegation } from "@prisma/client";
+import { resetDatabase } from "../../../../../integration-tests/helper";
+import { prisma } from "@td/prisma";
+import { GraphQLFormattedError } from "graphql";
+import { nowPlusXHours, todayAtMidnight, toddMMYYYY } from "../../../../utils";
+import { sendMail } from "../../../../mailer/mailing";
+import { renderMail, registryDelegationCreation } from "@td/mail";
+import { getStream } from "../../../../activity-events/data";
+
+// Mock emails
+jest.mock("../../../../mailer/mailing");
+(sendMail as jest.Mock).mockImplementation(() => Promise.resolve());
+
+const CREATE_REGISTRY_DELEGATION = gql`
+ mutation createRegistryDelegation($input: CreateRegistryDelegationInput!) {
+ createRegistryDelegation(input: $input) {
+ id
+ createdAt
+ updatedAt
+ delegate {
+ orgId
+ givenName
+ }
+ delegator {
+ orgId
+ }
+ startDate
+ endDate
+ comment
+ }
+ }
+`;
+
+interface CreateDelegation {
+ errors: readonly GraphQLFormattedError[];
+ data: Pick;
+ delegation?: RegistryDelegation;
+}
+export const createDelegation = async (
+ user: User | null,
+ input: CreateRegistryDelegationInput
+): Promise => {
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate<
+ Pick
+ >(CREATE_REGISTRY_DELEGATION, {
+ variables: {
+ input
+ }
+ });
+
+ if (errors) {
+ return { errors, data };
+ }
+
+ const delegation = await prisma.registryDelegation.findFirstOrThrow({
+ where: {
+ id: data.createRegistryDelegation.id
+ }
+ });
+
+ return { errors, data, delegation };
+};
+
+describe("mutation createRegistryDelegation", () => {
+ afterAll(resetDatabase);
+
+ afterEach(async () => {
+ jest.resetAllMocks();
+ });
+
+ describe("successful use-cases", () => {
+ it("should create a delegation declaration", async () => {
+ // Given
+ const delegate = await companyFactory({ givenName: "Some given name" });
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When
+ const { errors, data, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // Mutation return value should be OK
+ expect(data.createRegistryDelegation.delegator.orgId).toBe(
+ delegator.orgId
+ );
+ expect(data.createRegistryDelegation.delegate.orgId).toBe(delegate.orgId);
+ expect(data.createRegistryDelegation.delegate.givenName).toBe(
+ "Some given name"
+ );
+
+ // Persisted value should be OK
+ expect(delegation?.delegatorId).toBe(delegator.id);
+ expect(delegation?.delegateId).toBe(delegate.id);
+
+ // Should create an event
+ const eventsAfterCreate = await getStream(delegation!.id);
+ expect(eventsAfterCreate.length).toBe(1);
+ expect(eventsAfterCreate[0]).toMatchObject({
+ type: "RegistryDelegationCreated",
+ actor: user.id,
+ streamId: delegation!.id
+ });
+ });
+
+ it("should populate default values", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When
+ const { errors, data, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // Mutation return value should be OK
+ // Can't really do better for dates: https://github.com/prisma/prisma/issues/16719
+ expect(data.createRegistryDelegation.createdAt).not.toBeNull();
+ expect(data.createRegistryDelegation.updatedAt).not.toBeNull();
+ expect(data.createRegistryDelegation.startDate).toBe(
+ todayAtMidnight().toISOString()
+ );
+ expect(data.createRegistryDelegation.endDate).toBeNull();
+ expect(data.createRegistryDelegation.comment).toBeNull();
+
+ // Persisted value should be OK
+ expect(delegation?.createdAt).not.toBeNull();
+ expect(delegation?.updatedAt).not.toBeNull();
+ expect(delegation?.startDate.toISOString()).toBe(
+ todayAtMidnight().toISOString()
+ );
+ expect(delegation?.endDate).toBeNull();
+ expect(delegation?.comment).toBeNull();
+ expect(delegation?.revokedBy).toBeFalsy();
+ expect(delegation?.cancelledBy).toBeFalsy();
+ });
+
+ it("user can add a comment", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When
+ const COMMENT = "A super comment to explain delegation";
+ const { errors, data, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ comment: COMMENT
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // Mutation return value should be OK
+ expect(data.createRegistryDelegation.comment).toBe(COMMENT);
+
+ // Persisted value should be OK
+ expect(delegation?.comment).toBe(COMMENT);
+ });
+
+ it("test with null values", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When
+ const { errors, data, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ startDate: null,
+ endDate: null
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // Mutation return value should be OK
+ expect(data.createRegistryDelegation.delegator.orgId).toBe(
+ delegator.orgId
+ );
+ expect(data.createRegistryDelegation.delegate.orgId).toBe(delegate.orgId);
+
+ // Persisted value should be OK
+ expect(delegation?.delegatorId).toBe(delegator.id);
+ expect(delegation?.delegateId).toBe(delegate.id);
+ });
+
+ it("should send an email to companies admins", async () => {
+ // Given
+ const delegate = await companyFactory({ givenName: "Some given name" });
+ const { user: delegatorAdmin, company: delegator } =
+ await userWithCompanyFactory();
+ await userInCompany("MEMBER", delegator.id); // Not an admin, shoud not receive mail
+ await userInCompany("MEMBER", delegate.id); // Not an admin, shoud not receive mail
+ const delegateAdmin = await userInCompany("ADMIN", delegate.id); // Admin, should receive mail
+ await userWithCompanyFactory("ADMIN"); // Not part of the delegation, should not receive mail
+
+ // When
+ const { errors, delegation } = await createDelegation(delegatorAdmin, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // Email
+ jest.mock("../../../../mailer/mailing");
+ (sendMail as jest.Mock).mockImplementation(() => Promise.resolve());
+
+ expect(sendMail as jest.Mock).toHaveBeenCalledTimes(1);
+
+ // Onboarding email
+ expect(sendMail as jest.Mock).toHaveBeenCalledWith(
+ renderMail(registryDelegationCreation, {
+ variables: {
+ startDate: toddMMYYYY(delegation!.startDate!),
+ delegator,
+ delegate
+ },
+ messageVersions: [
+ {
+ to: expect.arrayContaining([
+ { email: delegatorAdmin.email, name: delegatorAdmin.name },
+ { email: delegateAdmin.email, name: delegateAdmin.name }
+ ])
+ }
+ ]
+ })
+ );
+ });
+ });
+
+ it("testing email content - no end date", async () => {
+ // Given
+ const delegate = await companyFactory({ givenName: "Some given name" });
+ const { user: delegatorAdmin, company: delegator } =
+ await userWithCompanyFactory();
+
+ // Email
+ jest.mock("../../../../mailer/mailing");
+ (sendMail as jest.Mock).mockImplementation(() => Promise.resolve());
+
+ // When
+ const { errors, delegation } = await createDelegation(delegatorAdmin, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(sendMail as jest.Mock).toHaveBeenCalledTimes(1);
+ expect(delegation).not.toBeUndefined();
+
+ if (!delegation) return;
+
+ const formattedStartDate = toddMMYYYY(delegation.startDate).replace(
+ /\//g,
+ "/"
+ );
+ expect(sendMail as jest.Mock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ body: expect.stringContaining(`effective à partir du ${formattedStartDate}
+ pour une durée illimitée. `),
+ subject: `Émission d'une demande de délégation de l'établissement ${delegator.name} (${delegator.siret})`
+ })
+ );
+ });
+
+ it("testing email content - with end date", async () => {
+ // Given
+ const delegate = await companyFactory({ givenName: "Some given name" });
+ const { user: delegatorAdmin, company: delegator } =
+ await userWithCompanyFactory();
+ const endDate = new Date("2050-10-10");
+
+ // Email
+ jest.mock("../../../../mailer/mailing");
+ (sendMail as jest.Mock).mockImplementation(() => Promise.resolve());
+
+ // When
+ const { errors, delegation } = await createDelegation(delegatorAdmin, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ endDate: endDate.toISOString() as any
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(sendMail as jest.Mock).toHaveBeenCalledTimes(1);
+
+ if (!delegation) return;
+
+ const formattedStartDate = toddMMYYYY(delegation.startDate).replace(
+ /\//g,
+ "/"
+ );
+ const formattedEndDate = toddMMYYYY(endDate).replace(/\//g, "/");
+ expect(sendMail as jest.Mock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ body: expect.stringContaining(`effective à partir du ${formattedStartDate}
+ et jusqu'au ${formattedEndDate}. `),
+ subject: `Émission d'une demande de délégation de l'établissement ${delegator.name} (${delegator.siret})`
+ })
+ );
+ });
+
+ describe("authentication & roles", () => {
+ it("user must be authenticated", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const delegator = await companyFactory();
+
+ // When
+ const { errors } = await createDelegation(null, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe("Vous n'êtes pas connecté.");
+ });
+
+ it("user must be admin", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory(
+ "MEMBER"
+ );
+
+ // When
+ const { errors } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "Vous n'avez pas les permissions suffisantes pour pouvoir créer une délégation."
+ );
+ });
+
+ it("user must belong to delegator company", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const delegator = await companyFactory();
+ const { user } = await userWithCompanyFactory("ADMIN");
+
+ // When
+ const { errors } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "Vous devez faire partie de l'entreprise délégante pour pouvoir créer une délégation."
+ );
+ });
+ });
+
+ describe("async validation", () => {
+ it("delegate company must exist", async () => {
+ // Given
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When
+ const { errors } = await createDelegation(user, {
+ delegateOrgId: "40081510600010",
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "L'entreprise 40081510600010 visée comme délégataire n'existe pas dans Trackdéchets"
+ );
+ });
+
+ it("delegator company must exist", async () => {
+ // Given
+ const { user, company: delegate } = await userWithCompanyFactory();
+
+ // When
+ const { errors } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: "40081510600010"
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "L'entreprise 40081510600010 visée comme délégante n'existe pas dans Trackdéchets"
+ );
+ });
+
+ it("delegator company must not be dormant", async () => {
+ // Given
+ const { user, company: delegator } = await userWithCompanyFactory(
+ "ADMIN",
+ { isDormantSince: new Date() }
+ );
+ const delegate = await companyFactory();
+
+ // When
+ const { errors } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ `L'entreprise ${delegator.siret} visée comme délégante est en sommeil`
+ );
+ });
+
+ it("delegate company must not be dormant", async () => {
+ // Given
+ const { user, company: delegator } = await userWithCompanyFactory();
+ const delegate = await companyFactory({ isDormantSince: new Date() });
+
+ // When
+ const { errors } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ `L'entreprise ${delegate.siret} visée comme délégataire est en sommeil`
+ );
+ });
+ });
+
+ describe("prevent simultaneous valid delegations", () => {
+ it("should throw if there is already an active delegation for those companies (no start date, no end date)", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When: create first delegation
+ const { errors, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // When: create second delegation
+ const { errors: errors2 } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors2).not.toBeUndefined();
+ expect(errors2[0].message).toBe(
+ `Une délégation existe déjà pour ce délégataire et ce délégant (id ${delegation?.id})`
+ );
+ });
+
+ it("should throw if there is already an active delegation for those companies (no start date, end date)", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When: create first delegation
+ const { errors, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ endDate: nowPlusXHours(2).toISOString() as any
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // When: create second delegation
+ const { errors: errors2 } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors2).not.toBeUndefined();
+ expect(errors2[0].message).toBe(
+ `Une délégation existe déjà pour ce délégataire et ce délégant (id ${delegation?.id})`
+ );
+ });
+
+ it("should throw if there is already an active delegation for those companies (start date, end date)", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When: create first delegation
+ const { errors, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ startDate: new Date().toISOString() as any,
+ endDate: nowPlusXHours(2).toISOString() as any
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // When: create second delegation
+ const { errors: errors2 } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors2).not.toBeUndefined();
+ expect(errors2[0].message).toBe(
+ `Une délégation existe déjà pour ce délégataire et ce délégant (id ${delegation?.id})`
+ );
+ });
+
+ it("should throw if there is already an existing delegation programmed in the future", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When: create first delegation
+ const { errors, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ startDate: nowPlusXHours(2).toISOString() as any,
+ endDate: nowPlusXHours(3).toISOString() as any
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // When: create second delegation
+ const { errors: errors2 } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors2).not.toBeUndefined();
+ expect(errors2[0].message).toBe(
+ `Une délégation existe déjà pour ce délégataire et ce délégant (id ${delegation?.id})`
+ );
+ });
+
+ it("should not throw if there is an overlapping delegation but it's been revoqued", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When: create first delegation
+ const { errors, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ endDate: nowPlusXHours(3).toISOString() as any
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // Refuse the delegation
+ await prisma.registryDelegation.update({
+ where: { id: delegation?.id },
+ data: { revokedBy: "someuserid" }
+ });
+
+ // When: create second delegation
+ const { errors: errors2 } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors2).toBeUndefined();
+ });
+
+ it("should not throw if there is an overlapping delegation but it's been cancelled", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When: create first delegation
+ const { errors, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ endDate: nowPlusXHours(3).toISOString() as any
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // Refuse the delegation
+ await prisma.registryDelegation.update({
+ where: { id: delegation?.id },
+ data: { cancelledBy: "someuserid" }
+ });
+
+ // When: create second delegation
+ const { errors: errors2 } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors2).toBeUndefined();
+ });
+
+ it("should not throw if there is an existing delegation in the future but it's been revoked", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When: create first delegation
+ const { errors, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ startDate: nowPlusXHours(2).toISOString() as any,
+ endDate: nowPlusXHours(3).toISOString() as any
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // Refuse the delegation
+ await prisma.registryDelegation.update({
+ where: { id: delegation?.id },
+ data: { revokedBy: "someuserid" }
+ });
+
+ // When: create second delegation
+ const { errors: errors2 } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors2).toBeUndefined();
+ });
+
+ it("should not throw if there is an existing delegation in the future but it's been cancelled", async () => {
+ // Given
+ const delegate = await companyFactory();
+ const { user, company: delegator } = await userWithCompanyFactory();
+
+ // When: create first delegation
+ const { errors, delegation } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId,
+ startDate: nowPlusXHours(2).toISOString() as any,
+ endDate: nowPlusXHours(3).toISOString() as any
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+
+ // Refuse the delegation
+ await prisma.registryDelegation.update({
+ where: { id: delegation?.id },
+ data: { cancelledBy: "someuserid" }
+ });
+
+ // When: create second delegation
+ const { errors: errors2 } = await createDelegation(user, {
+ delegateOrgId: delegate.orgId,
+ delegatorOrgId: delegator.orgId
+ });
+
+ // Then
+ expect(errors2).toBeUndefined();
+ });
+ });
+});
diff --git a/back/src/registryDelegation/resolvers/mutations/__tests__/revokeRegistryDelegation.integration.ts b/back/src/registryDelegation/resolvers/mutations/__tests__/revokeRegistryDelegation.integration.ts
new file mode 100644
index 0000000000..3e89fcfc34
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/mutations/__tests__/revokeRegistryDelegation.integration.ts
@@ -0,0 +1,197 @@
+import gql from "graphql-tag";
+import { resetDatabase } from "../../../../../integration-tests/helper";
+import { User } from "@prisma/client";
+import makeClient from "../../../../__tests__/testClient";
+import { Mutation } from "../../../../generated/graphql/types";
+import { prisma } from "@td/prisma";
+import { registryDelegationFactory } from "../../../__tests__/factories";
+import { userFactory, userInCompany } from "../../../../__tests__/factories";
+import { getStream } from "../../../../activity-events/data";
+
+const REVOKE_REGISTRY_DELEGATION = gql`
+ mutation revokeRegistryDelegation($delegationId: ID!) {
+ revokeRegistryDelegation(delegationId: $delegationId) {
+ id
+ status
+ delegate {
+ orgId
+ }
+ }
+ }
+`;
+
+const revokeDelegation = async (user: User | null, delegationId: string) => {
+ const { mutate } = makeClient(user);
+ const { errors, data } = await mutate<
+ Pick
+ >(REVOKE_REGISTRY_DELEGATION, {
+ variables: {
+ delegationId
+ }
+ });
+
+ const delegation = await prisma.registryDelegation.findFirst({
+ where: { id: delegationId }
+ });
+
+ return { data, errors, delegation };
+};
+
+describe("mutation revokeRegistryDelegation", () => {
+ afterAll(resetDatabase);
+
+ describe("successful use-cases", () => {
+ it("should revoke delegation", async () => {
+ // Given
+ const { delegation, delegatorUser, delegateCompany } =
+ await registryDelegationFactory();
+
+ // When
+ const {
+ errors,
+ data,
+ delegation: updatedDelegation
+ } = await revokeDelegation(delegatorUser, delegation.id);
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.revokeRegistryDelegation.status).toBe("REVOKED");
+ expect(data.revokeRegistryDelegation.delegate.orgId).toBe(
+ delegateCompany.orgId
+ );
+ expect(updatedDelegation?.revokedBy).toBe(delegatorUser.id);
+
+ // Should create an event
+ const eventsAfterCreate = await getStream(delegation!.id);
+ expect(eventsAfterCreate.length).toBe(1);
+ expect(eventsAfterCreate[0]).toMatchObject({
+ type: "RegistryDelegationUpdated",
+ actor: delegatorUser.id,
+ streamId: delegation!.id
+ });
+ });
+
+ it("delegator can revoke delegation", async () => {
+ // Given
+ const { delegation, delegatorUser } = await registryDelegationFactory();
+
+ // When
+ const {
+ errors,
+ data,
+ delegation: updatedDelegation
+ } = await revokeDelegation(delegatorUser, delegation.id);
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.revokeRegistryDelegation.status).toBe("REVOKED");
+ expect(updatedDelegation?.revokedBy).toBe(delegatorUser.id);
+ });
+ });
+
+ describe("authentication & roles", () => {
+ it("user must be authenticated", async () => {
+ // Given
+ const { delegation } = await registryDelegationFactory();
+
+ // When
+ const { errors } = await revokeDelegation(null, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe("Vous n'êtes pas connecté.");
+ });
+
+ it("user must belong to the delegator company", async () => {
+ // Given
+ const { delegation } = await registryDelegationFactory();
+ const user = await userFactory();
+
+ // When
+ const { errors } = await revokeDelegation(user, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "Vous devez faire partie de l'entreprise délégante d'une délégation pour pouvoir la révoquer."
+ );
+ });
+
+ it("user must be admin", async () => {
+ // Given
+ const { delegation, delegatorCompany } =
+ await registryDelegationFactory();
+ const user = await userInCompany("MEMBER", delegatorCompany.id);
+
+ // When
+ const { errors } = await revokeDelegation(user, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "Vous n'avez pas les permissions suffisantes pour pouvoir révoquer une délégation."
+ );
+ });
+
+ it("delegate can NOT revoke delegation", async () => {
+ // Given
+ const { delegation, delegateUser } = await registryDelegationFactory();
+
+ // When
+ const { errors } = await revokeDelegation(delegateUser, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "Vous devez faire partie de l'entreprise délégante d'une délégation pour pouvoir la révoquer."
+ );
+ });
+ });
+
+ describe("async validation", () => {
+ it("should throw if delegation does not exist", async () => {
+ // Given
+ const user = await userFactory();
+
+ // When
+ const { errors } = await revokeDelegation(
+ user,
+ "cxxxxxxxxxxxxxxxxxxxxxxxx"
+ );
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "La demande de délégation cxxxxxxxxxxxxxxxxxxxxxxxx n'existe pas."
+ );
+ });
+
+ it("cannot revoke an already revoked delegation", async () => {
+ // Given
+ const { delegation, delegatorUser } = await registryDelegationFactory({
+ revokedBy: "someuserid"
+ });
+
+ // When
+ const { errors } = await revokeDelegation(delegatorUser, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe("Cette délégation a déjà été révoquée.");
+ });
+
+ it("cannot revoke a cancelled delegation", async () => {
+ // Given
+ const { delegation, delegatorUser } = await registryDelegationFactory({
+ cancelledBy: "someuserid"
+ });
+
+ // When
+ const { errors } = await revokeDelegation(delegatorUser, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe("Cette délégation a été annulée.");
+ });
+ });
+});
diff --git a/back/src/registryDelegation/resolvers/mutations/cancelRegistryDelegation.ts b/back/src/registryDelegation/resolvers/mutations/cancelRegistryDelegation.ts
new file mode 100644
index 0000000000..c8d7f17765
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/mutations/cancelRegistryDelegation.ts
@@ -0,0 +1,41 @@
+import { applyAuthStrategies, AuthType } from "../../../auth";
+import { checkIsAuthenticated } from "../../../common/permissions";
+import {
+ ResolversParentTypes,
+ RegistryDelegation,
+ MutationCancelRegistryDelegationArgs
+} from "../../../generated/graphql/types";
+import { GraphQLContext } from "../../../types";
+import { checkCanCancel } from "../../permissions";
+import { parseMutationRevokeRegistryDelegationArgs } from "../../validation";
+import { fixTyping } from "../typing";
+import { findDelegationByIdOrThrow } from "../utils";
+import { cancelDelegation } from "./utils/cancelRegistryDelegation.utils";
+
+const cancelRegistryDelegation = async (
+ _: ResolversParentTypes["Mutation"],
+ args: MutationCancelRegistryDelegationArgs,
+ context: GraphQLContext
+): Promise => {
+ // Browser only
+ applyAuthStrategies(context, [AuthType.Session]);
+
+ // User must be authenticated
+ const user = checkIsAuthenticated(context);
+
+ // Sync validation of input
+ const { delegationId } = parseMutationRevokeRegistryDelegationArgs(args);
+
+ // Fetch delegation
+ const delegation = await findDelegationByIdOrThrow(user, delegationId);
+
+ // Make sure user can cancel delegation
+ await checkCanCancel(user, delegation);
+
+ // Cancel delegation
+ const cancelledDelegation = await cancelDelegation(user, delegation);
+
+ return fixTyping(cancelledDelegation);
+};
+
+export default cancelRegistryDelegation;
diff --git a/back/src/registryDelegation/resolvers/mutations/createRegistryDelegation.ts b/back/src/registryDelegation/resolvers/mutations/createRegistryDelegation.ts
new file mode 100644
index 0000000000..72342589c1
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/mutations/createRegistryDelegation.ts
@@ -0,0 +1,63 @@
+import { applyAuthStrategies, AuthType } from "../../../auth";
+import { checkIsAuthenticated } from "../../../common/permissions";
+import {
+ MutationCreateRegistryDelegationArgs,
+ ResolversParentTypes,
+ RegistryDelegation
+} from "../../../generated/graphql/types";
+import { GraphQLContext } from "../../../types";
+import { checkCanCreate } from "../../permissions";
+import { parseCreateRegistryDelegationInput } from "../../validation";
+import { fixTyping } from "../typing";
+import { findDelegateAndDelegatorOrThrow } from "../utils";
+import {
+ createDelegation,
+ checkNoExistingNotRevokedAndNotExpiredDelegation,
+ sendRegistryDelegationCreationEmail
+} from "./utils/createRegistryDelegation.utils";
+
+const createRegistryDelegation = async (
+ _: ResolversParentTypes["Mutation"],
+ { input }: MutationCreateRegistryDelegationArgs,
+ context: GraphQLContext
+): Promise => {
+ // Browser only
+ applyAuthStrategies(context, [AuthType.Session]);
+
+ // User must be authenticated
+ const user = checkIsAuthenticated(context);
+
+ // Sync validation of input
+ const delegationInput = parseCreateRegistryDelegationInput(input);
+
+ // Fetch companies
+ const { delegator, delegate } = await findDelegateAndDelegatorOrThrow(
+ delegationInput.delegateOrgId,
+ delegationInput.delegatorOrgId
+ );
+
+ // Make sure user can create delegation
+ await checkCanCreate(user, delegator);
+
+ // Check there's not already an existing delegation
+ await checkNoExistingNotRevokedAndNotExpiredDelegation(
+ user,
+ delegator,
+ delegate
+ );
+
+ // Create delegation
+ const delegation = await createDelegation(
+ user,
+ delegationInput,
+ delegator,
+ delegate
+ );
+
+ // Send email
+ await sendRegistryDelegationCreationEmail(delegation, delegator, delegate);
+
+ return fixTyping(delegation);
+};
+
+export default createRegistryDelegation;
diff --git a/back/src/registryDelegation/resolvers/mutations/revokeRegistryDelegation.ts b/back/src/registryDelegation/resolvers/mutations/revokeRegistryDelegation.ts
new file mode 100644
index 0000000000..9308ec2b2a
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/mutations/revokeRegistryDelegation.ts
@@ -0,0 +1,41 @@
+import { applyAuthStrategies, AuthType } from "../../../auth";
+import { checkIsAuthenticated } from "../../../common/permissions";
+import {
+ MutationRevokeRegistryDelegationArgs,
+ ResolversParentTypes,
+ RegistryDelegation
+} from "../../../generated/graphql/types";
+import { GraphQLContext } from "../../../types";
+import { checkCanRevoke } from "../../permissions";
+import { parseMutationRevokeRegistryDelegationArgs } from "../../validation";
+import { fixTyping } from "../typing";
+import { findDelegationByIdOrThrow } from "../utils";
+import { revokeDelegation } from "./utils/revokeRegistryDelegation.utils";
+
+const revokeRegistryDelegation = async (
+ _: ResolversParentTypes["Mutation"],
+ args: MutationRevokeRegistryDelegationArgs,
+ context: GraphQLContext
+): Promise => {
+ // Browser only
+ applyAuthStrategies(context, [AuthType.Session]);
+
+ // User must be authenticated
+ const user = checkIsAuthenticated(context);
+
+ // Sync validation of input
+ const { delegationId } = parseMutationRevokeRegistryDelegationArgs(args);
+
+ // Fetch delegation
+ const delegation = await findDelegationByIdOrThrow(user, delegationId);
+
+ // Make sure user can revoke delegation
+ await checkCanRevoke(user, delegation);
+
+ // Revoke delegation
+ const revokedDelegation = await revokeDelegation(user, delegation);
+
+ return fixTyping(revokedDelegation);
+};
+
+export default revokeRegistryDelegation;
diff --git a/back/src/registryDelegation/resolvers/mutations/utils/cancelRegistryDelegation.utils.ts b/back/src/registryDelegation/resolvers/mutations/utils/cancelRegistryDelegation.utils.ts
new file mode 100644
index 0000000000..b6049a62f3
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/mutations/utils/cancelRegistryDelegation.utils.ts
@@ -0,0 +1,13 @@
+import { RegistryDelegation } from "@prisma/client";
+import { getRegistryDelegationRepository } from "../../../repository";
+
+export const cancelDelegation = async (
+ user: Express.User,
+ delegation: RegistryDelegation
+) => {
+ const delegationRepository = getRegistryDelegationRepository(user);
+ return delegationRepository.update(
+ { id: delegation.id },
+ { cancelledBy: user.id }
+ );
+};
diff --git a/back/src/registryDelegation/resolvers/mutations/utils/createRegistryDelegation.utils.ts b/back/src/registryDelegation/resolvers/mutations/utils/createRegistryDelegation.utils.ts
new file mode 100644
index 0000000000..55c9d01a81
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/mutations/utils/createRegistryDelegation.utils.ts
@@ -0,0 +1,111 @@
+import { Company, RegistryDelegation } from "@prisma/client";
+import { UserInputError } from "../../../../common/errors";
+import { getRegistryDelegationRepository } from "../../../repository";
+import { ParsedCreateRegistryDelegationInput } from "../../../validation";
+import { prisma } from "@td/prisma";
+import { renderMail, registryDelegationCreation } from "@td/mail";
+import { sendMail } from "../../../../mailer/mailing";
+import { toddMMYYYY } from "../../../../utils";
+
+export const createDelegation = async (
+ user: Express.User,
+ input: ParsedCreateRegistryDelegationInput,
+ delegator: Company,
+ delegate: Company
+) => {
+ const delegationRepository = getRegistryDelegationRepository(user);
+ return delegationRepository.create({
+ startDate: input.startDate,
+ endDate: input.endDate,
+ comment: input.comment,
+ delegate: {
+ connect: {
+ id: delegate.id
+ }
+ },
+ delegator: {
+ connect: {
+ id: delegator.id
+ }
+ }
+ });
+};
+
+/**
+ * Check to prevent having multiple active delegations at the same time.
+ *
+ * We don't authorize already having a non-revoked, non-cancelled delegation that has
+ * not yet expired.
+ *
+ * That means that if the company has a delegation that only takes effect in the future,
+ * it cannot create a new one for the meantime. Users would have to delete the delegation
+ * in the future and create a new one.
+ */
+export const checkNoExistingNotRevokedAndNotExpiredDelegation = async (
+ user: Express.User,
+ delegator: Company,
+ delegate: Company
+) => {
+ const NOW = new Date();
+
+ const delegationRepository = getRegistryDelegationRepository(user);
+ const activeDelegation = await delegationRepository.findFirst({
+ delegatorId: delegator.id,
+ delegateId: delegate.id,
+ revokedBy: null,
+ cancelledBy: null,
+ OR: [{ endDate: null }, { endDate: { gt: NOW } }]
+ });
+
+ if (activeDelegation) {
+ throw new UserInputError(
+ `Une délégation existe déjà pour ce délégataire et ce délégant (id ${activeDelegation.id})`
+ );
+ }
+};
+
+/**
+ * Send a creation email to admin of both companies (delegate & delegator)
+ */
+export const sendRegistryDelegationCreationEmail = async (
+ delegation: RegistryDelegation,
+ delegator: Company,
+ delegate: Company
+) => {
+ // Find admins from both delegator & delegate companies
+ const companyAssociations = await prisma.companyAssociation.findMany({
+ where: {
+ companyId: { in: [delegator.id, delegate.id] },
+ role: "ADMIN"
+ },
+ include: {
+ user: {
+ select: {
+ id: true,
+ email: true,
+ name: true
+ }
+ }
+ }
+ });
+
+ // Prepare mail template
+ const payload = renderMail(registryDelegationCreation, {
+ variables: {
+ startDate: toddMMYYYY(delegation.startDate),
+ endDate: delegation.endDate ? toddMMYYYY(delegation.endDate) : undefined,
+ delegator,
+ delegate
+ },
+ messageVersions: [
+ {
+ to: companyAssociations.map(companyAssociation => ({
+ email: companyAssociation.user.email,
+ name: companyAssociation.user.name
+ }))
+ }
+ ]
+ });
+
+ await sendMail(payload);
+};
diff --git a/back/src/registryDelegation/resolvers/mutations/utils/revokeRegistryDelegation.utils.ts b/back/src/registryDelegation/resolvers/mutations/utils/revokeRegistryDelegation.utils.ts
new file mode 100644
index 0000000000..ecdc58f09d
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/mutations/utils/revokeRegistryDelegation.utils.ts
@@ -0,0 +1,13 @@
+import { RegistryDelegation } from "@prisma/client";
+import { getRegistryDelegationRepository } from "../../../repository";
+
+export const revokeDelegation = async (
+ user: Express.User,
+ delegation: RegistryDelegation
+) => {
+ const delegationRepository = getRegistryDelegationRepository(user);
+ return delegationRepository.update(
+ { id: delegation.id },
+ { revokedBy: user.id }
+ );
+};
diff --git a/back/src/registryDelegation/resolvers/queries/__tests__/registryDelegation.integration.ts b/back/src/registryDelegation/resolvers/queries/__tests__/registryDelegation.integration.ts
new file mode 100644
index 0000000000..072bf6b7e2
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/queries/__tests__/registryDelegation.integration.ts
@@ -0,0 +1,115 @@
+import gql from "graphql-tag";
+import { resetDatabase } from "../../../../../integration-tests/helper";
+import { registryDelegationFactory } from "../../../__tests__/factories";
+import makeClient from "../../../../__tests__/testClient";
+import { Query } from "../../../../generated/graphql/types";
+import { User } from "@prisma/client";
+import { userFactory } from "../../../../__tests__/factories";
+
+const REGISTRY_DELEGATION = gql`
+ query registryDelegation($delegationId: ID!) {
+ registryDelegation(delegationId: $delegationId) {
+ id
+ createdAt
+ updatedAt
+ delegate {
+ orgId
+ }
+ delegator {
+ orgId
+ }
+ startDate
+ endDate
+ comment
+ status
+ }
+ }
+`;
+
+export const getDelegation = async (
+ user: User | null,
+ delegationId: string
+) => {
+ const { query } = makeClient(user);
+ return query>(REGISTRY_DELEGATION, {
+ variables: {
+ delegationId
+ }
+ });
+};
+
+describe("query registryDelegation", () => {
+ afterAll(resetDatabase);
+
+ describe("successful use-cases", () => {
+ it("should return delegation", async () => {
+ // Given
+ const { delegation, delegatorUser, delegateCompany, delegatorCompany } =
+ await registryDelegationFactory();
+
+ // When
+ const { errors, data } = await getDelegation(
+ delegatorUser,
+ delegation.id
+ );
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.registryDelegation.id).toBe(delegation.id);
+ expect(data.registryDelegation.delegate.orgId).toBe(
+ delegateCompany.orgId
+ );
+ expect(data.registryDelegation.delegator.orgId).toBe(
+ delegatorCompany.orgId
+ );
+ });
+ });
+
+ describe("authentication & roles", () => {
+ it("user must be authenticated", async () => {
+ // Given
+ const { delegation } = await registryDelegationFactory();
+
+ // When
+ const { errors } = await getDelegation(null, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe("Vous n'êtes pas connecté.");
+ });
+
+ it("user must belong to delegate or delegator company", async () => {
+ // Given
+ const { delegation } = await registryDelegationFactory();
+ const user = await userFactory();
+
+ // When
+ const { errors } = await getDelegation(user, delegation.id);
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "Vous devez faire partie de l'entreprise délégante ou délégataire d'une délégation pour y avoir accès."
+ );
+ });
+ });
+
+ describe("async validation", () => {
+ it("should throw if delegation id does not exist", async () => {
+ // Given
+ const { delegatorUser } = await registryDelegationFactory();
+
+ // When
+ const { errors } = await getDelegation(
+ delegatorUser,
+ "cxxxxxxxxxxxxxxxxxxxxxxxx"
+ );
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ "La demande de délégation cxxxxxxxxxxxxxxxxxxxxxxxx n'existe pas."
+ );
+ });
+ });
+});
diff --git a/back/src/registryDelegation/resolvers/queries/__tests__/registryDelegations.integration.ts b/back/src/registryDelegation/resolvers/queries/__tests__/registryDelegations.integration.ts
new file mode 100644
index 0000000000..8e8faeb190
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/queries/__tests__/registryDelegations.integration.ts
@@ -0,0 +1,297 @@
+import gql from "graphql-tag";
+import { resetDatabase } from "../../../../../integration-tests/helper";
+import { User } from "@prisma/client";
+import makeClient from "../../../../__tests__/testClient";
+import {
+ Query,
+ QueryRegistryDelegationsArgs
+} from "../../../../generated/graphql/types";
+import {
+ registryDelegationFactory,
+ registryDelegationFactoryWithExistingCompanies
+} from "../../../__tests__/factories";
+import { prisma } from "@td/prisma";
+
+const REGISTRY_DELEGATIONS = gql`
+ query registryDelegations(
+ $skip: Int
+ $first: Int
+ $where: RegistryDelegationWhere
+ ) {
+ registryDelegations(skip: $skip, first: $first, where: $where) {
+ totalCount
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ edges {
+ node {
+ id
+ createdAt
+ updatedAt
+ delegate {
+ orgId
+ }
+ delegator {
+ orgId
+ }
+ startDate
+ endDate
+ comment
+ status
+ }
+ }
+ }
+ }
+`;
+
+export const getDelegations = async (
+ user: User | null,
+ paginationArgs: Partial
+) => {
+ const { query } = makeClient(user);
+ return query>(REGISTRY_DELEGATIONS, {
+ variables: {
+ ...paginationArgs
+ }
+ });
+};
+
+describe("query registryDelegations", () => {
+ afterAll(resetDatabase);
+
+ describe("successful use-cases", () => {
+ it("should return delegations", async () => {
+ // Given
+ const { delegation, delegatorUser, delegatorCompany, delegateCompany } =
+ await registryDelegationFactory();
+
+ // When
+ const { errors, data } = await getDelegations(delegatorUser, {
+ where: { delegatorOrgId: delegatorCompany.orgId }
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.registryDelegations.totalCount).toBe(1);
+ expect(data.registryDelegations.edges[0].node.id).toBe(delegation.id);
+ expect(data.registryDelegations.edges[0].node.delegate.orgId).toBe(
+ delegateCompany.orgId
+ );
+ expect(data.registryDelegations.edges[0].node.delegator.orgId).toBe(
+ delegatorCompany.orgId
+ );
+ });
+
+ it("user can query as delegate", async () => {
+ // Given
+ const { delegation, delegateUser, delegateCompany } =
+ await registryDelegationFactory();
+
+ // When
+ const { errors, data } = await getDelegations(delegateUser, {
+ where: { delegateOrgId: delegateCompany.orgId }
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.registryDelegations.totalCount).toBe(1);
+ expect(data.registryDelegations.edges[0].node.id).toBe(delegation.id);
+ });
+
+ it("user can query as delegator", async () => {
+ // Given
+ const { delegation, delegatorUser, delegatorCompany } =
+ await registryDelegationFactory();
+
+ // When
+ const { errors, data } = await getDelegations(delegatorUser, {
+ where: { delegatorOrgId: delegatorCompany.orgId }
+ });
+
+ // Then
+ expect(errors).toBeUndefined();
+ expect(data.registryDelegations.totalCount).toBe(1);
+ expect(data.registryDelegations.edges[0].node.id).toBe(delegation.id);
+ });
+ });
+
+ describe("authentication & roles", () => {
+ it("user must be authenticated", async () => {
+ // Given
+ const { delegatorCompany } = await registryDelegationFactory();
+
+ // When
+ const { errors } = await getDelegations(null, {
+ where: { delegatorOrgId: delegatorCompany.orgId }
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe("Vous n'êtes pas connecté.");
+ });
+
+ it("user must belong to delegate company", async () => {
+ // Given
+ const { delegateCompany, delegatorUser } =
+ await registryDelegationFactory();
+
+ // When
+ const { errors } = await getDelegations(delegatorUser, {
+ where: { delegateOrgId: delegateCompany.orgId }
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ `L'utilisateur ne fait pas partie de l'entreprise ${delegateCompany.orgId}.`
+ );
+ });
+
+ it("user must belong to delegator company", async () => {
+ // Given
+ const { delegatorCompany, delegateUser } =
+ await registryDelegationFactory();
+
+ // When
+ const { errors } = await getDelegations(delegateUser, {
+ where: { delegateOrgId: delegatorCompany.orgId }
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ `L'utilisateur ne fait pas partie de l'entreprise ${delegatorCompany.orgId}.`
+ );
+ });
+ });
+
+ describe("async validation", () => {
+ it("should throw if delegate company does not exist", async () => {
+ // Given
+ const { delegateUser } = await registryDelegationFactory();
+
+ // When
+ const { errors } = await getDelegations(delegateUser, {
+ where: { delegateOrgId: "39070205800012" }
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ `L'entreprise 39070205800012 n'existe pas.`
+ );
+ });
+
+ it("should throw if delegator company does not exist", async () => {
+ // Given
+ const { delegatorUser } = await registryDelegationFactory();
+
+ // When
+ const { errors } = await getDelegations(delegatorUser, {
+ where: { delegateOrgId: "39070205800012" }
+ });
+
+ // Then
+ expect(errors).not.toBeUndefined();
+ expect(errors[0].message).toBe(
+ `L'entreprise 39070205800012 n'existe pas.`
+ );
+ });
+ });
+
+ it("should be sorted by updatedAt", async () => {
+ // Given
+ const {
+ delegation: delegation1,
+ delegatorUser,
+ delegateCompany,
+ delegatorCompany
+ } = await registryDelegationFactory();
+ const delegation2 = await registryDelegationFactoryWithExistingCompanies(
+ delegateCompany,
+ delegatorCompany
+ );
+
+ // When
+ const { errors: errors1, data: data1 } = await getDelegations(
+ delegatorUser,
+ {
+ where: { delegatorOrgId: delegatorCompany.orgId }
+ }
+ );
+
+ // Then
+ expect(errors1).toBeUndefined();
+ expect(data1.registryDelegations.totalCount).toBe(2);
+ expect(data1.registryDelegations.edges[0].node.id).toBe(delegation2.id);
+ expect(data1.registryDelegations.edges[1].node.id).toBe(delegation1.id);
+
+ // When: update delegation 2
+ await prisma.registryDelegation.update({
+ where: { id: delegation1.id },
+ data: { comment: "test" }
+ });
+ const { errors: errors2, data: data2 } = await getDelegations(
+ delegatorUser,
+ {
+ where: { delegatorOrgId: delegatorCompany.orgId }
+ }
+ );
+
+ // Then
+ expect(errors2).toBeUndefined();
+ expect(data2.registryDelegations.totalCount).toBe(2);
+ expect(data2.registryDelegations.edges[0].node.id).toBe(delegation1.id);
+ expect(data2.registryDelegations.edges[1].node.id).toBe(delegation2.id);
+ });
+
+ it("should return paginated results", async () => {
+ // Given
+ const { delegation, delegatorUser, delegatorCompany, delegateCompany } =
+ await registryDelegationFactory();
+ const delegation2 = await registryDelegationFactoryWithExistingCompanies(
+ delegateCompany,
+ delegatorCompany
+ );
+ const delegation3 = await registryDelegationFactoryWithExistingCompanies(
+ delegateCompany,
+ delegatorCompany
+ );
+
+ // When (1st page)
+ const { errors: errors1, data: data1 } = await getDelegations(
+ delegatorUser,
+ {
+ where: { delegatorOrgId: delegatorCompany.orgId },
+ first: 2,
+ skip: 0
+ }
+ );
+
+ // Then
+ expect(errors1).toBeUndefined();
+ expect(data1.registryDelegations.totalCount).toBe(3);
+ expect(data1.registryDelegations.edges.length).toBe(2);
+ expect(data1.registryDelegations.edges[0].node.id).toBe(delegation3.id);
+ expect(data1.registryDelegations.edges[1].node.id).toBe(delegation2.id);
+
+ // When (second page)
+ const { errors: errors2, data: data2 } = await getDelegations(
+ delegatorUser,
+ {
+ where: { delegatorOrgId: delegatorCompany.orgId },
+ first: 2,
+ skip: 2
+ }
+ );
+
+ // Then
+ expect(errors2).toBeUndefined();
+ expect(data2.registryDelegations.totalCount).toBe(3);
+ expect(data2.registryDelegations.edges.length).toBe(1);
+ expect(data2.registryDelegations.edges[0].node.id).toBe(delegation.id);
+ });
+});
diff --git a/back/src/registryDelegation/resolvers/queries/registryDelegation.ts b/back/src/registryDelegation/resolvers/queries/registryDelegation.ts
new file mode 100644
index 0000000000..2ff31cd698
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/queries/registryDelegation.ts
@@ -0,0 +1,35 @@
+import { applyAuthStrategies, AuthType } from "../../../auth";
+import { checkIsAuthenticated } from "../../../common/permissions";
+import {
+ QueryResolvers,
+ RegistryDelegation
+} from "../../../generated/graphql/types";
+import { checkCanAccess } from "../../permissions";
+import { parseQueryRegistryDelegationArgs } from "../../validation";
+import { fixTyping } from "../typing";
+import { findDelegationByIdOrThrow } from "../utils";
+
+const registryDelegationResolver: QueryResolvers["registryDelegation"] = async (
+ _,
+ args,
+ context
+): Promise => {
+ // Browser only
+ applyAuthStrategies(context, [AuthType.Session]);
+
+ // User must be authenticated
+ const user = checkIsAuthenticated(context);
+
+ // Sync validation of args
+ const { delegationId } = parseQueryRegistryDelegationArgs(args);
+
+ // Fetch delegation
+ const delegation = await findDelegationByIdOrThrow(user, delegationId);
+
+ // Make sure user can access delegation
+ await checkCanAccess(user, delegation);
+
+ return fixTyping(delegation);
+};
+
+export default registryDelegationResolver;
diff --git a/back/src/registryDelegation/resolvers/queries/registryDelegations.ts b/back/src/registryDelegation/resolvers/queries/registryDelegations.ts
new file mode 100644
index 0000000000..947c095310
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/queries/registryDelegations.ts
@@ -0,0 +1,44 @@
+import { applyAuthStrategies, AuthType } from "../../../auth";
+import { checkIsAuthenticated } from "../../../common/permissions";
+import { QueryResolvers } from "../../../generated/graphql/types";
+import { checkBelongsTo } from "../../permissions";
+import { parseQueryRegistryDelegationsArgs } from "../../validation";
+import { fixPaginatedTyping } from "../typing";
+import { findDelegateOrDelegatorOrThrow } from "../utils";
+import { getPaginatedDelegations } from "./utils/registryDelegations.utils";
+
+const registryDelegationsResolver: QueryResolvers["registryDelegations"] =
+ async (_, args, context) => {
+ // Browser only
+ applyAuthStrategies(context, [AuthType.Session]);
+
+ // User must be authenticated
+ const user = checkIsAuthenticated(context);
+
+ // Sync validation of args
+ const paginationArgs = parseQueryRegistryDelegationsArgs(args);
+
+ const { delegateOrgId, delegatorOrgId } = paginationArgs.where;
+
+ // Find targeted company
+ const { delegate, delegator } = await findDelegateOrDelegatorOrThrow(
+ delegateOrgId,
+ delegatorOrgId
+ );
+
+ // Check that user belongs to company
+ if (delegate) await checkBelongsTo(user, delegate);
+ if (delegator) await checkBelongsTo(user, delegator);
+
+ // Get paginated delegations
+ const paginatedDelegations = await getPaginatedDelegations(user, {
+ delegate,
+ delegator,
+ skip: paginationArgs.skip,
+ first: paginationArgs.first
+ });
+
+ return fixPaginatedTyping(paginatedDelegations);
+ };
+
+export default registryDelegationsResolver;
diff --git a/back/src/registryDelegation/resolvers/queries/utils/registryDelegations.utils.ts b/back/src/registryDelegation/resolvers/queries/utils/registryDelegations.utils.ts
new file mode 100644
index 0000000000..e67967483a
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/queries/utils/registryDelegations.utils.ts
@@ -0,0 +1,52 @@
+import { Company } from "@prisma/client";
+import {
+ getConnection,
+ getPrismaPaginationArgs
+} from "../../../../common/pagination";
+import { getRegistryDelegationRepository } from "../../../repository";
+import { UserInputError } from "../../../../common/errors";
+
+interface Args {
+ delegate?: Company;
+ delegator?: Company;
+ skip?: number | null | undefined;
+ first?: number | null | undefined;
+}
+
+export const getPaginatedDelegations = async (
+ user: Express.User,
+ { delegate, delegator, skip, first }: Args
+) => {
+ if (!delegate && !delegator) {
+ throw new UserInputError(
+ "Vous devez préciser un délégant ou un délégataire"
+ );
+ }
+
+ const delegationRepository = getRegistryDelegationRepository(user);
+
+ const fixedWhere = {
+ delegateId: delegate?.id,
+ delegatorId: delegator?.id
+ };
+
+ const totalCount = await delegationRepository.count(fixedWhere);
+
+ const paginationArgs = getPrismaPaginationArgs({
+ skip: skip ?? 0,
+ first: first ?? 10
+ });
+
+ const result = await getConnection({
+ totalCount,
+ findMany: () =>
+ delegationRepository.findMany(fixedWhere, {
+ ...paginationArgs,
+ orderBy: { updatedAt: "desc" },
+ include: { delegator: true, delegate: true }
+ }),
+ formatNode: node => ({ ...node })
+ });
+
+ return result;
+};
diff --git a/back/src/registryDelegation/resolvers/subResolvers/RegistryDelegation.ts b/back/src/registryDelegation/resolvers/subResolvers/RegistryDelegation.ts
new file mode 100644
index 0000000000..fb74aba9ea
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/subResolvers/RegistryDelegation.ts
@@ -0,0 +1,12 @@
+import { RegistryDelegationResolvers } from "../../../generated/graphql/types";
+import { delegateResolver } from "./delegate";
+import { delegatorResolver } from "./delegator";
+import { statusResolver } from "./status";
+
+const registryDelegationResolvers: RegistryDelegationResolvers = {
+ status: statusResolver,
+ delegate: delegateResolver,
+ delegator: delegatorResolver
+};
+
+export default registryDelegationResolvers;
diff --git a/back/src/registryDelegation/resolvers/subResolvers/delegate.ts b/back/src/registryDelegation/resolvers/subResolvers/delegate.ts
new file mode 100644
index 0000000000..9b7cab66ca
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/subResolvers/delegate.ts
@@ -0,0 +1,19 @@
+import { prisma } from "@td/prisma";
+import {
+ RegistryDelegation,
+ RegistryDelegationResolvers
+} from "../../../generated/graphql/types";
+import { toGqlCompanyPrivate } from "../../../companies/converters";
+
+const getDelegate = async (delegation: RegistryDelegation) => {
+ const company = await prisma.registryDelegation
+ .findUniqueOrThrow({
+ where: { id: delegation.id }
+ })
+ .delegate();
+
+ return toGqlCompanyPrivate(company);
+};
+
+export const delegateResolver: RegistryDelegationResolvers["delegate"] =
+ delegation => getDelegate(delegation) as any;
diff --git a/back/src/registryDelegation/resolvers/subResolvers/delegator.ts b/back/src/registryDelegation/resolvers/subResolvers/delegator.ts
new file mode 100644
index 0000000000..10327a834b
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/subResolvers/delegator.ts
@@ -0,0 +1,19 @@
+import { prisma } from "@td/prisma";
+import {
+ RegistryDelegation,
+ RegistryDelegationResolvers
+} from "../../../generated/graphql/types";
+import { toGqlCompanyPrivate } from "../../../companies/converters";
+
+const getDelegator = async (delegation: RegistryDelegation) => {
+ const company = await prisma.registryDelegation
+ .findUniqueOrThrow({
+ where: { id: delegation.id }
+ })
+ .delegator();
+
+ return toGqlCompanyPrivate(company);
+};
+
+export const delegatorResolver: RegistryDelegationResolvers["delegator"] =
+ delegation => getDelegator(delegation) as any;
diff --git a/back/src/registryDelegation/resolvers/subResolvers/status.ts b/back/src/registryDelegation/resolvers/subResolvers/status.ts
new file mode 100644
index 0000000000..c82bae7f8d
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/subResolvers/status.ts
@@ -0,0 +1,17 @@
+import { RegistryDelegation } from "@prisma/client";
+import { RegistryDelegationResolvers } from "../../../generated/graphql/types";
+import { getDelegationStatus } from "../utils";
+
+/**
+ * For frontend's convenience, we compute the status here
+ */
+const getStatus = (delegation: RegistryDelegation) => {
+ return getDelegationStatus(delegation);
+};
+
+export const statusResolver: RegistryDelegationResolvers["status"] =
+ // Little cheat here on the typing, because we don't want to send the
+ // revokedBy & cancelledBy to the front so they are excluded from the GQL type,
+ // however we need them to compute the status. So we use the Prisma RegistryDelegation
+ // here, and force the typing
+ delegation => getStatus(delegation as unknown as RegistryDelegation);
diff --git a/back/src/registryDelegation/resolvers/typing.ts b/back/src/registryDelegation/resolvers/typing.ts
new file mode 100644
index 0000000000..b5ed7b7a22
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/typing.ts
@@ -0,0 +1,29 @@
+import { RegistryDelegation } from "@prisma/client";
+import {
+ CompanyPublic,
+ RegistryDelegationStatus
+} from "../../generated/graphql/types";
+
+// Revolvers don't provide some of the fields, because they are computed
+// by sub-resolvers (ie: status or delegate).
+// However, Apollo complains that the resolvers are missing said fields.
+// With this method we fake providing the sub-fields and Apollo shuts up.
+export const fixTyping = (delegation: RegistryDelegation) => {
+ return {
+ ...delegation,
+ delegator: null as unknown as CompanyPublic,
+ delegate: null as unknown as CompanyPublic,
+ status: null as unknown as RegistryDelegationStatus
+ };
+};
+
+// Same as above, but for a paginated output
+export const fixPaginatedTyping = paginatedDelegations => {
+ return {
+ ...paginatedDelegations,
+ edges: paginatedDelegations.edges.map(edge => ({
+ ...edge,
+ node: fixTyping(edge.node)
+ }))
+ };
+};
diff --git a/back/src/registryDelegation/resolvers/utils.ts b/back/src/registryDelegation/resolvers/utils.ts
new file mode 100644
index 0000000000..321242be03
--- /dev/null
+++ b/back/src/registryDelegation/resolvers/utils.ts
@@ -0,0 +1,105 @@
+import { prisma } from "@td/prisma";
+import { UserInputError } from "../../common/errors";
+import { getRegistryDelegationRepository } from "../repository";
+import { Company, Prisma, RegistryDelegation } from "@prisma/client";
+import { RegistryDelegationStatus } from "../../generated/graphql/types";
+
+export const findDelegateAndDelegatorOrThrow = async (
+ delegateOrgId: string,
+ delegatorOrgId: string
+) => {
+ const companies = await prisma.company.findMany({
+ where: {
+ orgId: { in: [delegateOrgId, delegatorOrgId] }
+ }
+ });
+
+ const delegator = companies.find(company => company.orgId === delegatorOrgId);
+ const delegate = companies.find(company => company.orgId === delegateOrgId);
+
+ if (!delegator) {
+ throw new UserInputError(
+ `L'entreprise ${delegatorOrgId} visée comme délégante n'existe pas dans Trackdéchets`
+ );
+ }
+
+ if (delegator.isDormantSince) {
+ throw new UserInputError(
+ `L'entreprise ${delegatorOrgId} visée comme délégante est en sommeil`
+ );
+ }
+
+ if (!delegate) {
+ throw new UserInputError(
+ `L'entreprise ${delegateOrgId} visée comme délégataire n'existe pas dans Trackdéchets`
+ );
+ }
+
+ if (delegate.isDormantSince) {
+ throw new UserInputError(
+ `L'entreprise ${delegateOrgId} visée comme délégataire est en sommeil`
+ );
+ }
+
+ return { delegator, delegate };
+};
+
+export const findCompanyByOrgIdOrThrow = async (companyOrgId: string) => {
+ const company = await prisma.company.findFirst({
+ where: { orgId: companyOrgId }
+ });
+
+ if (!company) {
+ throw new UserInputError(`L'entreprise ${companyOrgId} n'existe pas.`);
+ }
+
+ return company;
+};
+
+export const findDelegationByIdOrThrow = async (
+ user: Express.User,
+ id: string,
+ options?: Omit
+) => {
+ const delegationRepository = getRegistryDelegationRepository(user);
+ const delegation = await delegationRepository.findFirst({ id }, options);
+
+ if (!delegation) {
+ throw new UserInputError(`La demande de délégation ${id} n'existe pas.`);
+ }
+
+ return delegation;
+};
+
+export const findDelegateOrDelegatorOrThrow = async (
+ delegateOrgId?: string | null,
+ delegatorOrgId?: string | null
+): Promise<{ delegate?: Company; delegator?: Company }> => {
+ let delegate, delegator;
+
+ if (delegatorOrgId) {
+ delegator = await findCompanyByOrgIdOrThrow(delegatorOrgId);
+ }
+
+ if (delegateOrgId) {
+ delegate = await findCompanyByOrgIdOrThrow(delegateOrgId);
+ }
+
+ return { delegator, delegate };
+};
+
+export const getDelegationStatus = (delegation: RegistryDelegation) => {
+ const NOW = new Date();
+
+ const { revokedBy, cancelledBy, startDate, endDate } = delegation;
+
+ if (revokedBy) return "REVOKED" as RegistryDelegationStatus;
+
+ if (cancelledBy) return "CANCELLED" as RegistryDelegationStatus;
+
+ if (startDate > NOW) return "INCOMING" as RegistryDelegationStatus;
+
+ if (!endDate || endDate > NOW) return "ONGOING" as RegistryDelegationStatus;
+
+ return "EXPIRED" as RegistryDelegationStatus;
+};
diff --git a/back/src/registryDelegation/typeDefs/private/registryDelegation.enums.graphql b/back/src/registryDelegation/typeDefs/private/registryDelegation.enums.graphql
new file mode 100644
index 0000000000..eb887eb34c
--- /dev/null
+++ b/back/src/registryDelegation/typeDefs/private/registryDelegation.enums.graphql
@@ -0,0 +1,29 @@
+"""
+Statut d'une demande de délégation
+"""
+enum RegistryDelegationStatus {
+ """
+ À venir
+ """
+ INCOMING
+
+ """
+ En cours
+ """
+ ONGOING
+
+ """
+ Annulée
+ """
+ CANCELLED
+
+ """
+ Révoquée
+ """
+ REVOKED
+
+ """
+ Expirée
+ """
+ EXPIRED
+}
diff --git a/back/src/registryDelegation/typeDefs/private/registryDelegation.inputs.graphql b/back/src/registryDelegation/typeDefs/private/registryDelegation.inputs.graphql
new file mode 100644
index 0000000000..bb9e8b8509
--- /dev/null
+++ b/back/src/registryDelegation/typeDefs/private/registryDelegation.inputs.graphql
@@ -0,0 +1,39 @@
+"""
+Input pour la création d'une demande de délégation propre aux registres RNDTS
+"""
+input CreateRegistryDelegationInput {
+ """
+ OrgId du délégataire, c'est-à-dire l'entreprise qui fera les déclarations
+ à la place du délégant
+ """
+ delegateOrgId: String!
+
+ """
+ OrgId du délégant, c'est-à-dire l'entreprise qui délègue ses déclarations
+ aux délégataire
+ """
+ delegatorOrgId: String!
+
+ """
+ Date de début de la délégation. Par défaut, la date du jour
+ """
+ startDate: DateTime
+
+ """
+ Date de fin de la délégation. Si absente, la délégation est valable indéfiniment
+ """
+ endDate: DateTime
+
+ """
+ Objet de la délégation
+ """
+ comment: String
+}
+
+input RegistryDelegationWhere {
+ """
+ Delegate or delegator id. Specify exactly one of them
+ """
+ delegatorOrgId: ID
+ delegateOrgId: ID
+}
diff --git a/back/src/registryDelegation/typeDefs/private/registryDelegation.mutations.graphql b/back/src/registryDelegation/typeDefs/private/registryDelegation.mutations.graphql
new file mode 100644
index 0000000000..7592b933e7
--- /dev/null
+++ b/back/src/registryDelegation/typeDefs/private/registryDelegation.mutations.graphql
@@ -0,0 +1,21 @@
+type Mutation {
+ """
+ Permet de créer une demande de délégation pour déléguer les déclarations propres aux
+ registres RNDTS à un établissement.
+ """
+ createRegistryDelegation(
+ input: CreateRegistryDelegationInput!
+ ): RegistryDelegation!
+
+ """
+ Permet de révoquer une demande de délégation. Peut être utilisé par
+ le délégant. Une fois révoquée, une délégation ne peut plus être modifiée.
+ """
+ revokeRegistryDelegation(delegationId: ID!): RegistryDelegation!
+
+ """
+ Permet d'annuler une demande de délégation. Peut être utilisé par le délégataire.
+ Une fois annulée, une délégation ne peut plus être modifiée.
+ """
+ cancelRegistryDelegation(delegationId: ID!): RegistryDelegation!
+}
diff --git a/back/src/registryDelegation/typeDefs/private/registryDelegation.objects.graphql b/back/src/registryDelegation/typeDefs/private/registryDelegation.objects.graphql
new file mode 100644
index 0000000000..faa94cc94a
--- /dev/null
+++ b/back/src/registryDelegation/typeDefs/private/registryDelegation.objects.graphql
@@ -0,0 +1,66 @@
+"""
+Demande de délégation pour déléguer les déclarations propres aux
+registres RNDTS à un établissement.
+"""
+type RegistryDelegation {
+ """
+ Identifiant de la demande de délégation
+ """
+ id: String!
+
+ """
+ Date à laquelle la demande de délégation a été créée
+ """
+ createdAt: DateTime!
+
+ """
+ Date de dernière mise à jour de la demande de délégation
+ """
+ updatedAt: DateTime!
+
+ """
+ Délégataire, c'est-à-dire l'entreprise qui fera les déclarations
+ à la place du délégant
+ """
+ delegate: CompanyPublic!
+
+ """
+ Délégant, c'est-à-dire l'entreprise qui délègue ses déclarations
+ aux délégataire
+ """
+ delegator: CompanyPublic!
+
+ """
+ Date de début de la délégation
+ """
+ startDate: DateTime!
+
+ """
+ Date de fin de la délégation
+ """
+ endDate: DateTime
+
+ """
+ Objet de la délégation
+ """
+ comment: String
+
+ """
+ Statut de la délégation
+ """
+ status: RegistryDelegationStatus!
+}
+
+type RegistryDelegationEdge {
+ cursor: String!
+ node: RegistryDelegation!
+}
+
+"""
+List paginée de délégations
+"""
+type RegistryDelegationConnection {
+ totalCount: Int!
+ pageInfo: PageInfo!
+ edges: [RegistryDelegationEdge!]!
+}
diff --git a/back/src/registryDelegation/typeDefs/private/registryDelegation.queries.graphql b/back/src/registryDelegation/typeDefs/private/registryDelegation.queries.graphql
new file mode 100644
index 0000000000..9ab67d88b7
--- /dev/null
+++ b/back/src/registryDelegation/typeDefs/private/registryDelegation.queries.graphql
@@ -0,0 +1,15 @@
+type Query {
+ """
+ Renvoie une délégation, séléctionnée par son ID
+ """
+ registryDelegation(delegationId: ID!): RegistryDelegation!
+
+ """
+ Renvoie une liste de délégations (triée par updatedAt)
+ """
+ registryDelegations(
+ where: RegistryDelegationWhere
+ skip: Int
+ first: Int
+ ): RegistryDelegationConnection!
+}
diff --git a/back/src/registryDelegation/validation/__tests__/index.test.ts b/back/src/registryDelegation/validation/__tests__/index.test.ts
new file mode 100644
index 0000000000..ba16272eea
--- /dev/null
+++ b/back/src/registryDelegation/validation/__tests__/index.test.ts
@@ -0,0 +1,290 @@
+import {
+ parseCreateRegistryDelegationInput,
+ parseQueryRegistryDelegationArgs,
+ parseQueryRegistryDelegationsArgs
+} from "..";
+import { CreateRegistryDelegationInput } from "../../../generated/graphql/types";
+import { startOfDay } from "../../../utils";
+
+describe("index", () => {
+ describe("parseCreateRegistryDelegationInput", () => {
+ it("if no start date > default to today at midnight", () => {
+ // Given
+ const input: CreateRegistryDelegationInput = {
+ delegatorOrgId: "40081510600010",
+ delegateOrgId: "39070205800012"
+ };
+
+ // When
+ const delegation = parseCreateRegistryDelegationInput(input);
+
+ // Then
+ expect(delegation.delegatorOrgId).toBe("40081510600010");
+ expect(delegation.delegateOrgId).toBe("39070205800012");
+ expect(delegation.startDate?.toISOString()).toBe(
+ startOfDay(new Date()).toISOString()
+ );
+ expect(delegation.endDate).toBeUndefined();
+ });
+
+ it("should fix dates to midnight", () => {
+ // Given
+ const input: CreateRegistryDelegationInput = {
+ delegatorOrgId: "40081510600010",
+ delegateOrgId: "39070205800012",
+ startDate: new Date("2040-09-30T05:32:11.250Z"),
+ endDate: new Date("2050-09-30T05:32:11.250Z")
+ };
+
+ // When
+ const delegation = parseCreateRegistryDelegationInput(input);
+
+ // Then
+ expect(delegation.delegatorOrgId).toBe("40081510600010");
+ expect(delegation.delegateOrgId).toBe("39070205800012");
+ expect(delegation.startDate?.toISOString()).toBe(
+ "2040-09-30T00:00:00.000Z"
+ );
+ expect(delegation.endDate?.toISOString()).toBe(
+ "2050-09-30T23:59:59.999Z"
+ );
+ });
+
+ it("declaration with minimal info should be valid", () => {
+ // Given
+ const input: CreateRegistryDelegationInput = {
+ delegatorOrgId: "40081510600010",
+ delegateOrgId: "39070205800012"
+ };
+
+ // When
+ const delegation = parseCreateRegistryDelegationInput(input);
+
+ // Then
+ expect(delegation).toMatchObject({
+ delegatorOrgId: "40081510600010",
+ delegateOrgId: "39070205800012"
+ });
+ });
+
+ it("delegatorOrgId must be a valid orgId", () => {
+ // Given
+ const input: CreateRegistryDelegationInput = {
+ delegatorOrgId: "NOT-A-SIRET",
+ delegateOrgId: "39070205800012"
+ };
+
+ // When
+ expect.assertions(1);
+ try {
+ parseCreateRegistryDelegationInput(input);
+ } catch (error) {
+ // Then
+ expect(error.errors[0]).toMatchObject({
+ path: ["delegatorOrgId"],
+ message: "NOT-A-SIRET n'est pas un numéro de SIRET valide"
+ });
+ }
+ });
+
+ it("delegateOrgId must be a valid orgId", () => {
+ // Given
+ const input: CreateRegistryDelegationInput = {
+ delegatorOrgId: "40081510600010",
+ delegateOrgId: "NOT-A-SIRET"
+ };
+
+ // When
+ expect.assertions(1);
+ try {
+ parseCreateRegistryDelegationInput(input);
+ } catch (error) {
+ // Then
+ expect(error.errors[0]).toMatchObject({
+ path: ["delegateOrgId"],
+ message: "NOT-A-SIRET n'est pas un numéro de SIRET valide"
+ });
+ }
+ });
+
+ it("delegateOrgId must be different from delegatorOrgId", () => {
+ // Given
+ const input: CreateRegistryDelegationInput = {
+ delegatorOrgId: "40081510600010",
+ delegateOrgId: "40081510600010"
+ };
+
+ // When
+ expect.assertions(1);
+ try {
+ parseCreateRegistryDelegationInput(input);
+ } catch (error) {
+ // Then
+ expect(error.errors[0]).toMatchObject({
+ path: ["delegatorOrgId"],
+ message: "Le délégant et le délégataire doivent être différents."
+ });
+ }
+ });
+
+ it("validity start date cannot be in the past", () => {
+ // Given
+ const input: CreateRegistryDelegationInput = {
+ delegatorOrgId: "40081510600010",
+ delegateOrgId: "39070205800012",
+ startDate: new Date("2000-01-01")
+ };
+
+ // When
+ expect.assertions(1);
+ try {
+ parseCreateRegistryDelegationInput(input);
+ } catch (error) {
+ // Then
+ expect(error.errors[0]).toMatchObject({
+ path: ["startDate"],
+ message:
+ "La date de début de validité ne peut pas être dans le passé."
+ });
+ }
+ });
+
+ it("validity end date cannot be in the past", () => {
+ // Given
+ const input: CreateRegistryDelegationInput = {
+ delegatorOrgId: "40081510600010",
+ delegateOrgId: "39070205800012",
+ endDate: new Date("2000-01-01")
+ };
+
+ // When
+ expect.assertions(1);
+ try {
+ parseCreateRegistryDelegationInput(input);
+ } catch (error) {
+ // Then
+ expect(error.errors[0]).toMatchObject({
+ path: ["endDate"],
+ message: "La date de fin de validité ne peut pas être dans le passé."
+ });
+ }
+ });
+
+ it("validity end date cannot be before validity start date", () => {
+ // Given
+ const input: CreateRegistryDelegationInput = {
+ delegatorOrgId: "40081510600010",
+ delegateOrgId: "39070205800012",
+ startDate: new Date("2030-01-05"),
+ endDate: new Date("2030-01-01")
+ };
+
+ // When
+ expect.assertions(1);
+ try {
+ parseCreateRegistryDelegationInput(input);
+ } catch (error) {
+ // Then
+ expect(error.errors[0]).toMatchObject({
+ path: ["endDate"],
+ message:
+ "La date de début de validité doit être avant la date de fin."
+ });
+ }
+ });
+ });
+
+ describe("parseQueryRegistryDelegationArgs", () => {
+ it.each([null, undefined, 100, "no", "morethan25caracterssoitsinvalid"])(
+ "should throw if id is invalid (value = %p)",
+ delegationId => {
+ // Given
+
+ // When
+ expect.assertions(1);
+ try {
+ parseQueryRegistryDelegationArgs({
+ delegationId: delegationId as string
+ });
+ } catch (error) {
+ // Then
+ expect(error.errors[0]).toMatchObject({
+ path: ["delegationId"],
+ message: "L'id doit faire 25 caractères."
+ });
+ }
+ }
+ );
+
+ it("should return valid delegationId", () => {
+ // Given
+ const delegationId = "cl81ooom5138122w9sbznzdkg";
+
+ // When
+ const result = parseQueryRegistryDelegationArgs({ delegationId });
+
+ // Then
+ expect(result.delegationId).toBe(delegationId);
+ });
+ });
+
+ describe("parseQueryRegistryDelegationsArgs", () => {
+ it("should not allow passing where.delegator & where.delegate", () => {
+ // Given
+ const args = {
+ where: {
+ delegatorOrgId: "39070205800012",
+ delegateOrgId: "39070205800012"
+ }
+ };
+
+ // When
+ expect.assertions(1);
+ try {
+ parseQueryRegistryDelegationsArgs(args);
+ } catch (error) {
+ // Then
+ expect(error.errors[0]).toMatchObject({
+ path: ["where"],
+ message:
+ "Vous ne pouvez pas renseigner les deux champs (delegatorOrgId et delegateOrgId)."
+ });
+ }
+ });
+
+ it("should not allow empty where.delegator & where.delegate", () => {
+ // Given
+ const args = { where: {} };
+
+ // When
+ expect.assertions(1);
+ try {
+ parseQueryRegistryDelegationsArgs(args);
+ } catch (error) {
+ // Then
+ expect(error.errors[0]).toMatchObject({
+ path: ["where"],
+ message:
+ "Vous devez renseigner un des deux champs (delegatorOrgId ou delegateOrgId)."
+ });
+ }
+ });
+
+ it("should return valid args", () => {
+ // Given
+ const args = {
+ where: { delegatorOrgId: "39070205800012" },
+ skip: 0,
+ first: 10
+ };
+
+ // When
+ const { where, skip, first } = parseQueryRegistryDelegationsArgs(args);
+
+ // Then
+ expect(where).toMatchObject({ delegatorOrgId: "39070205800012" });
+ expect(skip).toBe(0);
+ expect(first).toBe(10);
+ });
+ });
+});
diff --git a/back/src/registryDelegation/validation/index.ts b/back/src/registryDelegation/validation/index.ts
new file mode 100644
index 0000000000..8471c9ecc8
--- /dev/null
+++ b/back/src/registryDelegation/validation/index.ts
@@ -0,0 +1,43 @@
+import {
+ CreateRegistryDelegationInput,
+ MutationRevokeRegistryDelegationArgs,
+ QueryRegistryDelegationArgs,
+ QueryRegistryDelegationsArgs
+} from "../../generated/graphql/types";
+import {
+ delegationIdSchema,
+ createRegistryDelegationInputSchema,
+ queryRegistryDelegationsArgsSchema
+} from "./schema";
+
+export function parseCreateRegistryDelegationInput(
+ input: CreateRegistryDelegationInput
+) {
+ return createRegistryDelegationInputSchema.parse(input);
+}
+
+export type ParsedCreateRegistryDelegationInput = ReturnType<
+ typeof parseCreateRegistryDelegationInput
+>;
+
+export function parseQueryRegistryDelegationArgs(
+ args: QueryRegistryDelegationArgs
+) {
+ return delegationIdSchema.parse(args);
+}
+
+export function parseMutationRevokeRegistryDelegationArgs(
+ args: MutationRevokeRegistryDelegationArgs
+) {
+ return delegationIdSchema.parse(args);
+}
+
+export function parseQueryRegistryDelegationsArgs(
+ args: QueryRegistryDelegationsArgs
+) {
+ return queryRegistryDelegationsArgsSchema.parse(args);
+}
+
+export type ParsedQueryRegistryDelegationsArgs = ReturnType<
+ typeof parseQueryRegistryDelegationsArgs
+>;
diff --git a/back/src/registryDelegation/validation/schema.ts b/back/src/registryDelegation/validation/schema.ts
new file mode 100644
index 0000000000..ae55a234d3
--- /dev/null
+++ b/back/src/registryDelegation/validation/schema.ts
@@ -0,0 +1,78 @@
+import { z } from "zod";
+import { siretSchema } from "../../common/validation/zod/schema";
+import { endOfDay, startOfDay, todayAtMidnight } from "../../utils";
+
+const idSchema = z.coerce.string().length(25, "L'id doit faire 25 caractères.");
+
+export const createRegistryDelegationInputSchema = z
+ .object({
+ delegateOrgId: siretSchema(),
+ delegatorOrgId: siretSchema(),
+ startDate: z.coerce
+ .date()
+ .nullish()
+ .transform((date): Date => {
+ // By default, today
+ if (!date) return todayAtMidnight();
+ // Else, chosen date at midnight
+ return startOfDay(date);
+ })
+ .refine(val => !val || val >= todayAtMidnight(), {
+ message: "La date de début de validité ne peut pas être dans le passé."
+ }),
+ endDate: z.coerce
+ .date()
+ .nullish()
+ .transform(date => {
+ if (!date) return date;
+ return endOfDay(date);
+ })
+ .refine(val => !val || val > new Date(), {
+ message: "La date de fin de validité ne peut pas être dans le passé."
+ }),
+ comment: z.string().max(500).optional()
+ })
+ .refine(
+ ({ delegateOrgId, delegatorOrgId }) => delegateOrgId !== delegatorOrgId,
+ {
+ path: ["delegatorOrgId"],
+ message: "Le délégant et le délégataire doivent être différents."
+ }
+ )
+ .refine(
+ data => {
+ const { startDate, endDate } = data;
+
+ if (startDate && endDate) {
+ return startDate < endDate;
+ }
+
+ return true;
+ },
+ {
+ path: ["endDate"],
+ message: "La date de début de validité doit être avant la date de fin."
+ }
+ );
+
+export const delegationIdSchema = z.object({
+ delegationId: idSchema
+});
+
+export const queryRegistryDelegationsArgsSchema = z.object({
+ where: z
+ .object({
+ delegatorOrgId: siretSchema().nullish(),
+ delegateOrgId: siretSchema().nullish()
+ })
+ .refine(
+ data => Boolean(data.delegatorOrgId) || Boolean(data.delegateOrgId),
+ "Vous devez renseigner un des deux champs (delegatorOrgId ou delegateOrgId)."
+ )
+ .refine(
+ data => !(Boolean(data.delegatorOrgId) && Boolean(data.delegateOrgId)),
+ "Vous ne pouvez pas renseigner les deux champs (delegatorOrgId et delegateOrgId)."
+ ),
+ skip: z.number().nonnegative().nullish(),
+ first: z.number().min(1).max(50).nullish()
+});
diff --git a/back/src/schema.ts b/back/src/schema.ts
index f6bb810b5f..678aa997ac 100644
--- a/back/src/schema.ts
+++ b/back/src/schema.ts
@@ -14,6 +14,7 @@ import registerResolvers from "./registry/resolvers";
import applicationResolvers from "./applications/resolvers";
import webhookResolvers from "./webhooks/resolvers";
import companyDigestResolvers from "./companydigest/resolvers";
+import registryDelegationResolvers from "./registryDelegation/resolvers";
// Merge GraphQL schema by merging types definitions and resolvers
// from differents modules
@@ -32,7 +33,8 @@ const repositories = [
"registry",
"applications",
"webhooks",
- "companydigest"
+ "companydigest",
+ "registryDelegation"
];
const typeDefsPath = repositories.map(
@@ -57,7 +59,8 @@ const resolvers = [
registerResolvers,
applicationResolvers,
webhookResolvers,
- companyDigestResolvers
+ companyDigestResolvers,
+ registryDelegationResolvers
];
export { typeDefs, resolvers };
diff --git a/back/src/scripts/bin/cleanUpIsReturnForTab.ts b/back/src/scripts/bin/cleanUpIsReturnForTab.ts
new file mode 100644
index 0000000000..2274037fb4
--- /dev/null
+++ b/back/src/scripts/bin/cleanUpIsReturnForTab.ts
@@ -0,0 +1,15 @@
+import { logger } from "@td/logger";
+import { cleanUpIsReturnForTab } from "../../common/elasticHelpers";
+
+(async () => {
+ try {
+ const body = await cleanUpIsReturnForTab();
+
+ logger.info(
+ `[cleanUpIsReturnForTab] Update ended! ${body.updated} bsds updated in ${body.took}ms!`
+ );
+ } catch (error) {
+ logger.error("Error in cleanUpIsReturnForTab script, exiting", error);
+ throw new Error(`Error in cleanUpIsReturnForTab script : ${error}`);
+ }
+})();
diff --git a/back/src/users/__tests__/database.integration.ts b/back/src/users/__tests__/database.integration.ts
index e56d8e230d..c19bbc8be7 100644
--- a/back/src/users/__tests__/database.integration.ts
+++ b/back/src/users/__tests__/database.integration.ts
@@ -8,6 +8,8 @@ import {
} from "../../__tests__/factories";
import { associateUserToCompany, createUserAccountHash } from "../database";
import { getUserRoles } from "../../permissions";
+import { UserRole } from "@prisma/client";
+import { getDefaultNotifications } from "../notifications";
describe("createUserAccountHash", () => {
afterAll(resetDatabase);
@@ -50,7 +52,7 @@ describe("associateUserToCompany", () => {
it("should throw error if association already exists", async () => {
const { user, company } = await userWithCompanyFactory("MEMBER");
try {
- await associateUserToCompany(user.id, company.siret, "MEMBER");
+ await associateUserToCompany(user.id, company.orgId, "MEMBER");
} catch (err) {
expect(err.extensions.code).toEqual(ErrorCode.BAD_USER_INPUT);
expect(err.message).toEqual(
@@ -59,16 +61,29 @@ describe("associateUserToCompany", () => {
}
});
- it("should associate an user to a company", async () => {
- const user = await userFactory();
- const company = await companyFactory();
+ it.each([UserRole.ADMIN, UserRole.MEMBER, UserRole.READER, UserRole.DRIVER])(
+ "should associate a user to a company with tole %p",
+ async role => {
+ const user = await userFactory();
+ const company = await companyFactory();
- await associateUserToCompany(user.id, company.siret, "MEMBER");
- const refreshedUser = await prisma.user.findUniqueOrThrow({
- where: { id: user.id }
- });
- const userRoles = await getUserRoles(user.id);
- expect(userRoles).toEqual({ [company.orgId]: "MEMBER" });
- expect(refreshedUser.firstAssociationDate).toBeTruthy();
- });
+ await associateUserToCompany(user.id, company.orgId, role);
+ const refreshedUser = await prisma.user.findUniqueOrThrow({
+ where: { id: user.id }
+ });
+ const companyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+ expect(companyAssociation.role).toEqual(role);
+
+ const expectedNotifications = getDefaultNotifications(role);
+
+ expect(companyAssociation.notifications).toEqual(expectedNotifications);
+
+ const userRoles = await getUserRoles(user.id);
+ expect(userRoles).toEqual({ [company.orgId]: role });
+ expect(refreshedUser.firstAssociationDate).toBeTruthy();
+ }
+ );
});
diff --git a/back/src/users/database.ts b/back/src/users/database.ts
index bc61f1c851..451e377b37 100644
--- a/back/src/users/database.ts
+++ b/back/src/users/database.ts
@@ -17,6 +17,7 @@ import { deleteCachedUserRoles } from "../common/redis/users";
import { hashPassword, passwordVersion } from "./utils";
import { UserInputError } from "../common/errors";
import { PrismaTransaction } from "../common/repository/types";
+import { ALL_NOTIFICATIONS, getDefaultNotifications } from "./notifications";
export async function getUserCompanies(userId: string): Promise {
const companyAssociations = await prisma.companyAssociation.findMany({
@@ -87,9 +88,9 @@ export async function deleteUserAccountHash(
* @param role
*/
export async function associateUserToCompany(
- userId,
- orgId,
- role,
+ userId: string,
+ orgId: string,
+ role: UserRole,
opt: Partial = {}
) {
// check for current associations
@@ -110,11 +111,14 @@ export async function associateUserToCompany(
);
}
+ const notifications = getDefaultNotifications(role);
+
const association = await prisma.companyAssociation.create({
data: {
user: { connect: { id: userId } },
role,
company: { connect: { orgId } },
+ notifications,
...opt
}
});
@@ -203,7 +207,9 @@ export async function acceptNewUserCompanyInvitations(user: User) {
data: {
company: { connect: { orgId: existingHash.companySiret } },
user: { connect: { id: user.id } },
- role: existingHash.role
+ role: existingHash.role,
+ notifications:
+ existingHash.role === UserRole.ADMIN ? ALL_NOTIFICATIONS : []
}
})
)
diff --git a/back/src/users/notifications.ts b/back/src/users/notifications.ts
new file mode 100644
index 0000000000..f6a3e8574d
--- /dev/null
+++ b/back/src/users/notifications.ts
@@ -0,0 +1,70 @@
+import { UserNotification, UserRole } from "@prisma/client";
+import { Recipient } from "@td/mail";
+import { prisma } from "@td/prisma";
+
+// Récupère la liste des des utilisateurs abonnés à un type
+// de notification donnée au sein d'un ou plusieurs établissements
+export async function getNotificationSubscribers(
+ notification: UserNotification,
+ orgIds: string[]
+): Promise {
+ const companies = await prisma.company.findMany({
+ where: { orgId: { in: orgIds } },
+ include: {
+ companyAssociations: {
+ where: {
+ notifications: { has: notification },
+ user: {
+ isActive: true
+ }
+ },
+ include: {
+ user: { select: { name: true, email: true } }
+ }
+ }
+ }
+ });
+
+ return companies.flatMap(c => c.companyAssociations.map(a => a.user));
+}
+
+/**
+ * Renvoie les notififications auxquelles un utilisateur est abonné par
+ * défaut lorsqu'il rejoint un établissement ou change de rôle
+ */
+export function getDefaultNotifications(role: UserRole) {
+ return role === UserRole.ADMIN ? ALL_NOTIFICATIONS : [];
+}
+
+// if you modify this structure, please modify
+// in front/src/common/notifications
+export const ALL_NOTIFICATIONS: UserNotification[] = [
+ UserNotification.MEMBERSHIP_REQUEST,
+ UserNotification.REVISION_REQUEST,
+ UserNotification.BSD_REFUSAL,
+ UserNotification.SIGNATURE_CODE_RENEWAL,
+ UserNotification.BSDA_FINAL_DESTINATION_UPDATE
+];
+
+// if you modify this structure, please modify
+// in front/src/common/notifications
+export const authorizedNotifications = {
+ [UserRole.ADMIN]: ALL_NOTIFICATIONS,
+ [UserRole.MEMBER]: [
+ UserNotification.REVISION_REQUEST,
+ UserNotification.BSD_REFUSAL,
+ UserNotification.SIGNATURE_CODE_RENEWAL,
+ UserNotification.BSDA_FINAL_DESTINATION_UPDATE
+ ],
+ [UserRole.READER]: [
+ UserNotification.REVISION_REQUEST,
+ UserNotification.BSD_REFUSAL,
+ UserNotification.SIGNATURE_CODE_RENEWAL,
+ UserNotification.BSDA_FINAL_DESTINATION_UPDATE
+ ],
+ DRIVER: [
+ UserNotification.BSD_REFUSAL,
+ UserNotification.SIGNATURE_CODE_RENEWAL,
+ UserNotification.BSDA_FINAL_DESTINATION_UPDATE
+ ]
+};
diff --git a/back/src/users/resolvers/Mutation.ts b/back/src/users/resolvers/Mutation.ts
index 96b8205c96..f86ecec4ef 100644
--- a/back/src/users/resolvers/Mutation.ts
+++ b/back/src/users/resolvers/Mutation.ts
@@ -19,6 +19,7 @@ import revokeAllAccessTokens from "./mutations/revokeAllAccessTokens";
import resetPassword from "./mutations/resetPassword";
import anonymizeUser from "./mutations/anonymizeUser";
import changeUserRole from "./mutations/changeUserRole";
+import setCompanyNotifications from "./mutations/setCompanyNotifications";
const Mutation: MutationResolvers = {
signup,
@@ -40,7 +41,8 @@ const Mutation: MutationResolvers = {
revokeAccessToken,
createAccessToken,
revokeAllAccessTokens,
- changeUserRole
+ changeUserRole,
+ setCompanyNotifications
};
export default Mutation;
diff --git a/back/src/users/resolvers/User.ts b/back/src/users/resolvers/User.ts
index 2885e86964..aa0534b207 100644
--- a/back/src/users/resolvers/User.ts
+++ b/back/src/users/resolvers/User.ts
@@ -1,7 +1,7 @@
import { libelleFromCodeNaf } from "../../companies/sirene/utils";
-import { convertUrls } from "../../companies/database";
import { UserResolvers, CompanyPrivate } from "../../generated/graphql/types";
import { prisma } from "@td/prisma";
+import { toGqlCompanyPrivate } from "../../companies/converters";
const userResolvers: UserResolvers = {
// Returns the list of companies a user belongs to
@@ -18,7 +18,7 @@ const userResolvers: UserResolvers = {
}));
return companies.map(async company => {
- const companyPrivate: CompanyPrivate = convertUrls(company);
+ const companyPrivate: CompanyPrivate = toGqlCompanyPrivate(company);
const { codeNaf: naf, address } = company;
const libelleNaf = libelleFromCodeNaf(naf!);
diff --git a/back/src/users/resolvers/mutations/__tests__/acceptMembershipRequest.integration.ts b/back/src/users/resolvers/mutations/__tests__/acceptMembershipRequest.integration.ts
index 10ef4a135b..8a223e4b64 100644
--- a/back/src/users/resolvers/mutations/__tests__/acceptMembershipRequest.integration.ts
+++ b/back/src/users/resolvers/mutations/__tests__/acceptMembershipRequest.integration.ts
@@ -9,6 +9,8 @@ import {
import makeClient from "../../../../__tests__/testClient";
import { renderMail, membershipRequestAccepted } from "@td/mail";
import { Mutation } from "../../../../generated/graphql/types";
+import { UserRole } from "@prisma/client";
+import { ALL_NOTIFICATIONS } from "../../../notifications";
// No mails
jest.mock("../../../../mailer/mailing");
@@ -112,7 +114,7 @@ describe("mutation acceptMembershipRequest", () => {
);
});
- it.each(["MEMBER", "ADMIN"])(
+ it.each([UserRole.ADMIN, UserRole.MEMBER, UserRole.READER, UserRole.DRIVER])(
"should associate requesting user to company with role %p",
async role => {
const { user, company } = await userWithCompanyFactory("ADMIN");
@@ -151,6 +153,12 @@ describe("mutation acceptMembershipRequest", () => {
expect(companyAssociations).toHaveLength(1);
expect(companyAssociations[0].role).toEqual(role);
+ const expectedNotifications = role === "ADMIN" ? ALL_NOTIFICATIONS : [];
+
+ expect(companyAssociations[0].notifications).toEqual(
+ expectedNotifications
+ );
+
// when a new user is invited and accepts invitation, `automaticallyAccepted` is false
expect(companyAssociations[0].automaticallyAccepted).toEqual(false);
diff --git a/back/src/users/resolvers/mutations/__tests__/changeUserRole.integration.ts b/back/src/users/resolvers/mutations/__tests__/changeUserRole.integration.ts
index b67bf8b927..42254ca280 100644
--- a/back/src/users/resolvers/mutations/__tests__/changeUserRole.integration.ts
+++ b/back/src/users/resolvers/mutations/__tests__/changeUserRole.integration.ts
@@ -9,6 +9,8 @@ import { hash } from "bcrypt";
import { AuthType } from "../../../../auth";
import { Mutation } from "../../../../generated/graphql/types";
import { ErrorCode, NotCompanyAdminErrorMsg } from "../../../../common/errors";
+import { UserRole } from "@prisma/client";
+import { ALL_NOTIFICATIONS } from "../../../notifications";
const CHANGE_USER_ROLE = `
mutation ChangeUserRole($userId: ID!, $orgId: ID!, $role: UserRole!){
@@ -22,36 +24,155 @@ const CHANGE_USER_ROLE = `
describe("mutation changeUserRole", () => {
afterAll(resetDatabase);
- test("admin can change a company user's role", async () => {
- const { user: admin, company } = await userWithCompanyFactory("ADMIN");
- const userToModify = await userFactory({
- companyAssociations: {
- create: {
- company: { connect: { id: company.id } },
- role: "MEMBER"
+ test.each([UserRole.MEMBER, UserRole.READER, UserRole.DRIVER])(
+ "admin can change a company user with role ADMIN to role %p",
+ async role => {
+ const { user: admin, company } = await userWithCompanyFactory("ADMIN");
+ const userToModify = await userFactory({
+ companyAssociations: {
+ create: {
+ company: { connect: { id: company.id } },
+ role: UserRole.ADMIN,
+ notifications: ALL_NOTIFICATIONS
+ }
}
- }
- });
+ });
- const { mutate } = makeClient({ ...admin, auth: AuthType.Session });
- const { data } = await mutate>(
- CHANGE_USER_ROLE,
- {
- variables: {
- userId: userToModify.id,
- orgId: company.orgId,
- role: "ADMIN"
+ const { mutate } = makeClient({ ...admin, auth: AuthType.Session });
+ const { data } = await mutate>(
+ CHANGE_USER_ROLE,
+ {
+ variables: {
+ userId: userToModify.id,
+ orgId: company.orgId,
+ role
+ }
}
- }
- );
- expect(data.changeUserRole.email).toEqual(userToModify.email);
- expect(data.changeUserRole.role).toEqual("ADMIN");
- const companyAssociations = await prisma.user
- .findUniqueOrThrow({ where: { id: userToModify.id } })
- .companyAssociations();
- expect(companyAssociations).toHaveLength(1);
- expect(companyAssociations[0].role).toEqual("ADMIN");
- });
+ );
+ expect(data.changeUserRole.email).toEqual(userToModify.email);
+ expect(data.changeUserRole.role).toEqual(role);
+ const companyAssociations = await prisma.user
+ .findUniqueOrThrow({ where: { id: userToModify.id } })
+ .companyAssociations();
+ expect(companyAssociations).toHaveLength(1);
+ expect(companyAssociations[0].role).toEqual(role);
+ expect(companyAssociations[0].notifications).toEqual([]);
+ }
+ );
+
+ test.each([UserRole.ADMIN, UserRole.READER, UserRole.DRIVER])(
+ "admin can change a company user with role MEMBER to role %p",
+ async role => {
+ const { user: admin, company } = await userWithCompanyFactory("ADMIN");
+ const userToModify = await userFactory({
+ companyAssociations: {
+ create: {
+ company: { connect: { id: company.id } },
+ role: UserRole.MEMBER,
+ notifications: []
+ }
+ }
+ });
+
+ const { mutate } = makeClient({ ...admin, auth: AuthType.Session });
+ const { data } = await mutate>(
+ CHANGE_USER_ROLE,
+ {
+ variables: {
+ userId: userToModify.id,
+ orgId: company.orgId,
+ role
+ }
+ }
+ );
+ expect(data.changeUserRole.email).toEqual(userToModify.email);
+ expect(data.changeUserRole.role).toEqual(role);
+ const companyAssociations = await prisma.user
+ .findUniqueOrThrow({ where: { id: userToModify.id } })
+ .companyAssociations();
+ expect(companyAssociations).toHaveLength(1);
+ expect(companyAssociations[0].role).toEqual(role);
+ expect(companyAssociations[0].notifications).toEqual(
+ role === UserRole.ADMIN ? ALL_NOTIFICATIONS : []
+ );
+ }
+ );
+
+ test.each([UserRole.ADMIN, UserRole.MEMBER, UserRole.DRIVER])(
+ "admin can change a company user with role READER to role %p",
+ async role => {
+ const { user: admin, company } = await userWithCompanyFactory("ADMIN");
+ const userToModify = await userFactory({
+ companyAssociations: {
+ create: {
+ company: { connect: { id: company.id } },
+ role: UserRole.READER,
+ notifications: []
+ }
+ }
+ });
+
+ const { mutate } = makeClient({ ...admin, auth: AuthType.Session });
+ const { data } = await mutate>(
+ CHANGE_USER_ROLE,
+ {
+ variables: {
+ userId: userToModify.id,
+ orgId: company.orgId,
+ role
+ }
+ }
+ );
+ expect(data.changeUserRole.email).toEqual(userToModify.email);
+ expect(data.changeUserRole.role).toEqual(role);
+ const companyAssociations = await prisma.user
+ .findUniqueOrThrow({ where: { id: userToModify.id } })
+ .companyAssociations();
+ expect(companyAssociations).toHaveLength(1);
+ expect(companyAssociations[0].role).toEqual(role);
+ expect(companyAssociations[0].notifications).toEqual(
+ role === UserRole.ADMIN ? ALL_NOTIFICATIONS : []
+ );
+ }
+ );
+
+ test.each([UserRole.ADMIN, UserRole.MEMBER, UserRole.READER])(
+ "admin can change a company user with role DRIVER to role %p",
+ async role => {
+ const { user: admin, company } = await userWithCompanyFactory("ADMIN");
+ const userToModify = await userFactory({
+ companyAssociations: {
+ create: {
+ company: { connect: { id: company.id } },
+ role: UserRole.DRIVER,
+ notifications: []
+ }
+ }
+ });
+
+ const { mutate } = makeClient({ ...admin, auth: AuthType.Session });
+ const { data } = await mutate>(
+ CHANGE_USER_ROLE,
+ {
+ variables: {
+ userId: userToModify.id,
+ orgId: company.orgId,
+ role
+ }
+ }
+ );
+ expect(data.changeUserRole.email).toEqual(userToModify.email);
+ expect(data.changeUserRole.role).toEqual(role);
+ const companyAssociations = await prisma.user
+ .findUniqueOrThrow({ where: { id: userToModify.id } })
+ .companyAssociations();
+ expect(companyAssociations).toHaveLength(1);
+ expect(companyAssociations[0].role).toEqual(role);
+ expect(companyAssociations[0].notifications).toEqual(
+ role === UserRole.ADMIN ? ALL_NOTIFICATIONS : []
+ );
+ }
+ );
test("admin can change an invited user's role", async () => {
const { user: admin, company } = await userWithCompanyFactory("ADMIN");
diff --git a/back/src/users/resolvers/mutations/__tests__/inviteUserToCompany.integration.ts b/back/src/users/resolvers/mutations/__tests__/inviteUserToCompany.integration.ts
index cf7b8da6dd..84415599c6 100644
--- a/back/src/users/resolvers/mutations/__tests__/inviteUserToCompany.integration.ts
+++ b/back/src/users/resolvers/mutations/__tests__/inviteUserToCompany.integration.ts
@@ -10,6 +10,9 @@ import { prisma } from "@td/prisma";
import { AuthType } from "../../../../auth";
import { Mutation } from "../../../../generated/graphql/types";
import { ErrorCode, NotCompanyAdminErrorMsg } from "../../../../common/errors";
+import { UserRole } from "@prisma/client";
+import { templateIds } from "@td/mail";
+import { getDefaultNotifications } from "../../../notifications";
const INVITE_USER_TO_COMPANY = `
mutation InviteUserToCompany($email: String!, $siret: String!, $role: UserRole!){
@@ -32,95 +35,119 @@ beforeEach(() => {
describe("mutation inviteUserToCompany", () => {
afterAll(resetDatabase);
- test("admin user can invite existing user to company", async () => {
- const { user: admin, company } = await userWithCompanyFactory("ADMIN");
- const user = await userFactory();
- const { mutate } = makeClient({ ...admin, auth: AuthType.Session });
- const { data } = await mutate>(
- INVITE_USER_TO_COMPANY,
- {
- variables: { email: user.email, siret: company.siret, role: "MEMBER" }
- }
- );
- expect(data.inviteUserToCompany.users!.length).toBe(2);
- expect(data.inviteUserToCompany.users).toEqual(
- expect.arrayContaining([{ email: admin.email }, { email: user.email }])
- );
- const companyAssociations = await prisma.user
- .findUniqueOrThrow({ where: { id: user.id } })
- .companyAssociations();
- expect(companyAssociations).toHaveLength(1);
- expect(companyAssociations[0].role).toEqual("MEMBER");
-
- // when invited user was already on TD, `automaticallyAccepted` is true
- expect(companyAssociations[0].automaticallyAccepted).toEqual(true);
- expect(companyAssociations[0].createdAt).toBeTruthy();
-
- const userCompany = await prisma.companyAssociation
- .findUniqueOrThrow({
- where: {
- id: companyAssociations[0].id
+ test.each([
+ UserRole.ADMIN,
+ UserRole.MEMBER,
+ UserRole.READER,
+ UserRole.DRIVER
+ ])(
+ "admin user can invite existing user to company with role %p",
+ async role => {
+ const { user: admin, company } = await userWithCompanyFactory("ADMIN");
+ const user = await userFactory();
+ const { mutate } = makeClient({ ...admin, auth: AuthType.Session });
+ const { data } = await mutate>(
+ INVITE_USER_TO_COMPANY,
+ {
+ variables: { email: user.email, siret: company.siret, role }
}
- })
- .company();
- expect(userCompany?.siret).toEqual(company.siret);
- expect(userCompany?.siret).toEqual(company.siret);
- });
-
- test("admin user can invite a new user to a company", async () => {
- // set up an user, a company, its admin and an invitation (UserAccountHash)
- const { user: admin, company } = await userWithCompanyFactory("ADMIN");
-
- const { mutate } = makeClient({ ...admin, auth: AuthType.Session });
-
- // Call the mutation to send an invitation
- const invitedUserEmail = "newuser@example.test";
- await mutate(INVITE_USER_TO_COMPANY, {
- variables: {
- email: invitedUserEmail,
- siret: company.siret,
- role: "MEMBER"
- }
- });
-
- // Check userAccountHash has been successfully created
- const hashes = await prisma.userAccountHash.findMany({
- where: { email: invitedUserEmail, companySiret: company.siret! }
- });
- expect(hashes.length).toEqual(1);
-
- // Check email was sent
- const hashValue = hashes[0].hash;
-
- // Check that the job was added to the queue
- expect(addToMailQueue as jest.Mock as jest.Mock).toHaveBeenCalledTimes(
- 1
- );
-
- const addJobArgs: any = (addToMailQueue as jest.Mock).mock.calls[0];
-
- // the right payload
- expect(addJobArgs[0]).toMatchObject({
- subject: "Vous avez été invité à rejoindre Trackdéchets",
- templateId: 9,
- to: [{ email: "newuser@example.test", name: "newuser@example.test" }],
- vars: {
- API_URL: "http://api.trackdechets.local",
- UI_URL: "http://trackdechets.local",
- companyName: company.name,
- companyOrgId: company.siret,
- hash: encodeURIComponent(hashValue)
- }
- });
- expect(addJobArgs[0].body).toContain(
- `vous a invité à rejoindre\n Trackdéchets`
- );
- expect(addJobArgs[0].body).toContain(
- ``
- );
- });
+ );
+ expect(data.inviteUserToCompany.users!.length).toBe(2);
+ expect(data.inviteUserToCompany.users).toEqual(
+ expect.arrayContaining([{ email: admin.email }, { email: user.email }])
+ );
+ const companyAssociations = await prisma.user
+ .findUniqueOrThrow({ where: { id: user.id } })
+ .companyAssociations();
+ expect(companyAssociations).toHaveLength(1);
+ expect(companyAssociations[0].role).toEqual(role);
+
+ // when invited user was already on TD, `automaticallyAccepted` is true
+ expect(companyAssociations[0].automaticallyAccepted).toEqual(true);
+ expect(companyAssociations[0].createdAt).toBeTruthy();
+
+ const expectedEmailNotification = getDefaultNotifications(role);
+
+ expect(companyAssociations[0].notifications).toEqual(
+ expectedEmailNotification
+ );
+
+ const userCompany = await prisma.companyAssociation
+ .findUniqueOrThrow({
+ where: {
+ id: companyAssociations[0].id
+ }
+ })
+ .company();
+ expect(userCompany?.siret).toEqual(company.siret);
+ expect(userCompany?.siret).toEqual(company.siret);
+ }
+ );
+
+ test.each([
+ UserRole.ADMIN,
+ UserRole.MEMBER,
+ UserRole.READER,
+ UserRole.DRIVER
+ ])(
+ "admin user can invite a new user to a company with role %p",
+ async role => {
+ // set up an user, a company, its admin and an invitation (UserAccountHash)
+ const { user: admin, company } = await userWithCompanyFactory("ADMIN");
+
+ const { mutate } = makeClient({ ...admin, auth: AuthType.Session });
+
+ // Call the mutation to send an invitation
+ const invitedUserEmail = "newuser@example.test";
+ await mutate(INVITE_USER_TO_COMPANY, {
+ variables: {
+ email: invitedUserEmail,
+ siret: company.siret,
+ role
+ }
+ });
+
+ // Check userAccountHash has been successfully created
+ const hashes = await prisma.userAccountHash.findMany({
+ where: { email: invitedUserEmail, companySiret: company.siret! }
+ });
+ expect(hashes.length).toEqual(1);
+
+ expect(hashes[0].role).toEqual(role);
+
+ // Check email was sent
+ const hashValue = hashes[0].hash;
+
+ // Check that the job was added to the queue
+ expect(
+ addToMailQueue as jest.Mock as jest.Mock
+ ).toHaveBeenCalledTimes(1);
+
+ const addJobArgs: any = (addToMailQueue as jest.Mock).mock.calls[0];
+
+ // the right payload
+ expect(addJobArgs[0]).toMatchObject({
+ subject: "Vous avez été invité à rejoindre Trackdéchets",
+ templateId: templateIds.LAYOUT,
+ to: [{ email: "newuser@example.test", name: "newuser@example.test" }],
+ vars: {
+ API_URL: "http://api.trackdechets.local",
+ UI_URL: "http://trackdechets.local",
+ companyName: company.name,
+ companyOrgId: company.siret,
+ hash: encodeURIComponent(hashValue)
+ }
+ });
+ expect(addJobArgs[0].body).toContain(
+ `vous a invité à rejoindre\n Trackdéchets`
+ );
+ expect(addJobArgs[0].body).toContain(
+ ``
+ );
+ }
+ );
test("TD admin user can invite a new user to a company", async () => {
const company = await companyFactory();
diff --git a/back/src/users/resolvers/mutations/__tests__/joinWithInvite.integration.ts b/back/src/users/resolvers/mutations/__tests__/joinWithInvite.integration.ts
index 8bad71374f..08bf645c8f 100644
--- a/back/src/users/resolvers/mutations/__tests__/joinWithInvite.integration.ts
+++ b/back/src/users/resolvers/mutations/__tests__/joinWithInvite.integration.ts
@@ -4,6 +4,8 @@ import { companyFactory } from "../../../../__tests__/factories";
import { getUserCompanies } from "../../../database";
import makeClient from "../../../../__tests__/testClient";
import { Mutation } from "../../../../generated/graphql/types";
+import { UserRole } from "@prisma/client";
+import { getDefaultNotifications } from "../../../notifications";
const JOIN_WITH_INVITE = `
mutation JoinWithInvite($inviteHash: String!, $name: String!, $password: String!){
@@ -57,56 +59,64 @@ describe("joinWithInvite mutation", () => {
expect(errors[0].message).toEqual("Cette invitation a déjà été acceptée");
});
- it("should create user, associate it to company and mark invitation as joined", async () => {
- const company = await companyFactory();
- const invitee = "john.snow@trackdechets.fr";
-
- const invitation = await prisma.userAccountHash.create({
- data: {
- email: invitee,
- companySiret: company.siret!,
- role: "MEMBER",
- hash: "hash"
- }
- });
-
- const { data } = await mutate>(
- JOIN_WITH_INVITE,
- {
- variables: {
- inviteHash: invitation.hash,
- name: "John Snow",
- password: "password"
+ it.each([UserRole.ADMIN, UserRole.MEMBER, UserRole.READER, UserRole.DRIVER])(
+ "should create user, associate it to company with role %p and mark invitation as joined",
+ async role => {
+ const company = await companyFactory();
+ const invitee = "john.snow@trackdechets.fr";
+
+ const invitation = await prisma.userAccountHash.create({
+ data: {
+ email: invitee,
+ companySiret: company.siret!,
+ role,
+ hash: "hash"
}
- }
- );
+ });
+
+ const { data } = await mutate>(
+ JOIN_WITH_INVITE,
+ {
+ variables: {
+ inviteHash: invitation.hash,
+ name: "John Snow",
+ password: "password"
+ }
+ }
+ );
- expect(data.joinWithInvite.email).toEqual(invitee);
+ expect(data.joinWithInvite.email).toEqual(invitee);
- // should mark invitation as joined
- const updatedInvitation = await prisma.userAccountHash.findUniqueOrThrow({
- where: {
- id: invitation.id
- }
- });
- expect(updatedInvitation.acceptedAt).not.toBeNull();
+ // should mark invitation as joined
+ const updatedInvitation = await prisma.userAccountHash.findUniqueOrThrow({
+ where: {
+ id: invitation.id
+ }
+ });
+ expect(updatedInvitation.acceptedAt).not.toBeNull();
- // check invitee is company member
- const isCompanyMember =
- (await prisma.companyAssociation.findFirst({
+ const companyAssociation = await prisma.companyAssociation.findFirst({
where: {
user: { email: invitee },
company: { siret: company.siret }
}
- })) != null;
- expect(isCompanyMember).toEqual(true);
+ });
- const createdUser = await prisma.user.findUniqueOrThrow({
- where: { email: invitee }
- });
- expect(createdUser.activatedAt).toBeTruthy();
- expect(createdUser.firstAssociationDate).toBeTruthy();
- });
+ // check invitee is company member
+ expect(companyAssociation).not.toBeNull();
+ expect(companyAssociation?.role).toEqual(role);
+
+ const expectedNotifications = getDefaultNotifications(role);
+
+ expect(companyAssociation?.notifications).toEqual(expectedNotifications);
+
+ const createdUser = await prisma.user.findUniqueOrThrow({
+ where: { email: invitee }
+ });
+ expect(createdUser.activatedAt).toBeTruthy();
+ expect(createdUser.firstAssociationDate).toBeTruthy();
+ }
+ );
it("should accept other pending invitations", async () => {
const company1 = await companyFactory();
diff --git a/back/src/users/resolvers/mutations/__tests__/sendMembershipRequest.integration.ts b/back/src/users/resolvers/mutations/__tests__/sendMembershipRequest.integration.ts
index 3ce3a23e2e..e565189731 100644
--- a/back/src/users/resolvers/mutations/__tests__/sendMembershipRequest.integration.ts
+++ b/back/src/users/resolvers/mutations/__tests__/sendMembershipRequest.integration.ts
@@ -16,6 +16,7 @@ import {
} from "@td/mail";
import { Mutation } from "../../../../generated/graphql/types";
import { subMinutes } from "date-fns";
+import { UserNotification } from "@prisma/client";
// No mails
jest.mock("../../../../mailer/mailing");
@@ -54,8 +55,19 @@ describe("mutation sendMembershipRequest", () => {
email: "john.snow@trackdechets.fr",
createdAt: subMinutes(new Date(), 5)
});
+ const admin2 = await userFactory({
+ email: "aria.starck@trackdechets.fr",
+ createdAt: subMinutes(new Date(), 5)
+ });
const company = await companyFactory();
- await associateUserToCompany(admin.id, company.siret, "ADMIN");
+ // this user should receive the notification
+ await associateUserToCompany(admin.id, company.orgId, "ADMIN", {
+ notifications: [UserNotification.MEMBERSHIP_REQUEST]
+ });
+ // this user should not receive the notification
+ await associateUserToCompany(admin2.id, company.orgId, "ADMIN", {
+ notifications: []
+ });
const { mutate } = makeClient(requester);
const { data } = await mutate>(
SEND_MEMBERSHIP_REQUEST,
@@ -127,7 +139,7 @@ describe("mutation sendMembershipRequest", () => {
createdAt: subMinutes(new Date(), 5)
});
const company = await companyFactory();
- await associateUserToCompany(admin.id, company.siret, "ADMIN");
+ await associateUserToCompany(admin.id, company.orgId, "ADMIN");
const { mutate } = makeClient(requester);
const { data } = await mutate>(
SEND_MEMBERSHIP_REQUEST,
@@ -248,7 +260,7 @@ describe("mutation sendMembershipRequest", () => {
email: `admin${userIndex}@trackdechets.fr`
});
const company = await companyFactory();
- await associateUserToCompany(admin.id, company.siret, "ADMIN");
+ await associateUserToCompany(admin.id, company.orgId, "ADMIN");
await prisma.membershipRequest.create({
data: {
diff --git a/back/src/users/resolvers/mutations/__tests__/setCompanyNotifications.integration.ts b/back/src/users/resolvers/mutations/__tests__/setCompanyNotifications.integration.ts
new file mode 100644
index 0000000000..8f42836d64
--- /dev/null
+++ b/back/src/users/resolvers/mutations/__tests__/setCompanyNotifications.integration.ts
@@ -0,0 +1,308 @@
+import gql from "graphql-tag";
+import {
+ companyFactory,
+ userFactory,
+ userWithCompanyFactory
+} from "../../../../__tests__/factories";
+import makeClient from "../../../../__tests__/testClient";
+import {
+ Mutation,
+ MutationSetCompanyNotificationsArgs
+} from "../../../../generated/graphql/types";
+import { prisma } from "@td/prisma";
+import { UserNotification, UserRole } from "@prisma/client";
+import { ALL_NOTIFICATIONS } from "../../../notifications";
+
+export const SET_COMPANY_NOTIFICATIONS = gql`
+ mutation SetCompanyNotifications($input: SetCompanyNotificationsInput!) {
+ setCompanyNotifications(input: $input) {
+ id
+ orgId
+ userNotifications
+ }
+ }
+`;
+
+describe("Mutation { setCompanyNotifications }", () => {
+ test("Users who don't belong to company cannot subscribe to notifications", async () => {
+ const user = await userFactory();
+ const company = await companyFactory();
+
+ const { mutate } = makeClient(user);
+
+ const newNotifications = [UserNotification.MEMBERSHIP_REQUEST];
+
+ const { errors } = await mutate<
+ Pick,
+ MutationSetCompanyNotificationsArgs
+ >(SET_COMPANY_NOTIFICATIONS, {
+ variables: {
+ input: {
+ companyOrgId: company.siret!,
+ notifications: newNotifications
+ }
+ }
+ });
+
+ expect(errors).toEqual([
+ expect.objectContaining({
+ message: `Vous n'êtes pas membre de l'entreprise portant le siret "${company.siret}".`
+ })
+ ]);
+ });
+
+ test.each(ALL_NOTIFICATIONS)(
+ "User with role ADMIN can subscribe to notification %p",
+ async notification => {
+ const { user, company } = await userWithCompanyFactory(UserRole.ADMIN);
+
+ const companyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ expect(companyAssociation.notifications).toEqual(ALL_NOTIFICATIONS);
+
+ const { mutate } = makeClient(user);
+
+ const newNotifications = [notification];
+
+ const { data, errors } = await mutate<
+ Pick,
+ MutationSetCompanyNotificationsArgs
+ >(SET_COMPANY_NOTIFICATIONS, {
+ variables: {
+ input: {
+ companyOrgId: company.orgId,
+ notifications: newNotifications
+ }
+ }
+ });
+
+ expect(errors).toBeUndefined();
+
+ expect(data.setCompanyNotifications.userNotifications).toEqual(
+ newNotifications
+ );
+
+ const updatedCompanyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ expect(updatedCompanyAssociation.notifications).toEqual(newNotifications);
+ }
+ );
+
+ test.each([
+ UserNotification.REVISION_REQUEST,
+ UserNotification.BSD_REFUSAL,
+ UserNotification.SIGNATURE_CODE_RENEWAL,
+ UserNotification.BSDA_FINAL_DESTINATION_UPDATE
+ ])(
+ "User with role MEMBER can subscribe to notification %p",
+ async notification => {
+ const { user, company } = await userWithCompanyFactory(UserRole.MEMBER);
+
+ const companyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ expect(companyAssociation.notifications).toEqual([]);
+
+ const { mutate } = makeClient(user);
+
+ const newNotifications = [notification];
+
+ const { data, errors } = await mutate<
+ Pick,
+ MutationSetCompanyNotificationsArgs
+ >(SET_COMPANY_NOTIFICATIONS, {
+ variables: {
+ input: {
+ companyOrgId: company.orgId,
+ notifications: newNotifications
+ }
+ }
+ });
+
+ expect(errors).toBeUndefined();
+ expect(data.setCompanyNotifications.userNotifications).toEqual(
+ newNotifications
+ );
+
+ const updatedCompanyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ expect(updatedCompanyAssociation.notifications).toEqual(newNotifications);
+ }
+ );
+
+ test.each([
+ UserNotification.REVISION_REQUEST,
+ UserNotification.BSD_REFUSAL,
+ UserNotification.SIGNATURE_CODE_RENEWAL,
+ UserNotification.BSDA_FINAL_DESTINATION_UPDATE
+ ])(
+ "User with role READER can subscribe to notification %p",
+ async notification => {
+ const { user, company } = await userWithCompanyFactory(UserRole.READER);
+
+ const companyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ expect(companyAssociation.notifications).toEqual([]);
+
+ const { mutate } = makeClient(user);
+
+ const newNotifications = [notification];
+
+ const { data, errors } = await mutate<
+ Pick,
+ MutationSetCompanyNotificationsArgs
+ >(SET_COMPANY_NOTIFICATIONS, {
+ variables: {
+ input: {
+ companyOrgId: company.orgId,
+ notifications: newNotifications
+ }
+ }
+ });
+
+ expect(errors).toBeUndefined();
+ expect(data.setCompanyNotifications.userNotifications).toEqual(
+ newNotifications
+ );
+
+ const updatedCompanyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ expect(updatedCompanyAssociation.notifications).toEqual(newNotifications);
+ }
+ );
+
+ test.each([
+ UserNotification.BSD_REFUSAL,
+ UserNotification.SIGNATURE_CODE_RENEWAL,
+ UserNotification.BSDA_FINAL_DESTINATION_UPDATE
+ ])(
+ "User with role DRIVER can subscribe to notification %p",
+ async notification => {
+ const { user, company } = await userWithCompanyFactory(UserRole.DRIVER);
+
+ const companyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ expect(companyAssociation.notifications).toEqual([]);
+
+ const { mutate } = makeClient(user);
+
+ const newNotifications = [notification];
+
+ const { data, errors } = await mutate<
+ Pick,
+ MutationSetCompanyNotificationsArgs
+ >(SET_COMPANY_NOTIFICATIONS, {
+ variables: {
+ input: {
+ companyOrgId: company.orgId,
+ notifications: newNotifications
+ }
+ }
+ });
+
+ expect(errors).toBeUndefined();
+ expect(data.setCompanyNotifications.userNotifications).toEqual(
+ newNotifications
+ );
+
+ const updatedCompanyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ expect(updatedCompanyAssociation.notifications).toEqual(newNotifications);
+ }
+ );
+
+ test.each([UserRole.MEMBER, UserRole.DRIVER, UserRole.READER])(
+ "users with role %p should not be able to subscribe to MEMBERSHIP_REQUEST notifications",
+ async role => {
+ const { user, company } = await userWithCompanyFactory(role);
+
+ const companyAssociation =
+ await prisma.companyAssociation.findFirstOrThrow({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ expect(companyAssociation.notifications).toEqual([]);
+
+ const { mutate } = makeClient(user);
+
+ const newNotifications = [UserNotification.MEMBERSHIP_REQUEST];
+
+ const { errors } = await mutate<
+ Pick,
+ MutationSetCompanyNotificationsArgs
+ >(SET_COMPANY_NOTIFICATIONS, {
+ variables: {
+ input: {
+ companyOrgId: company.orgId,
+ notifications: newNotifications
+ }
+ }
+ });
+
+ expect(errors).toEqual([
+ expect.objectContaining({
+ message:
+ "Votre rôle au sein de l'établissement ne vous permet pas de recevoir les notifications de type MEMBERSHIP_REQUEST"
+ })
+ ]);
+ }
+ );
+
+ test("Users with role DRIVER should not be able to susbcribe to REVISION_REQUEST notifications", async () => {
+ const { user, company } = await userWithCompanyFactory(UserRole.DRIVER);
+
+ const companyAssociation = await prisma.companyAssociation.findFirstOrThrow(
+ {
+ where: { companyId: company.id, userId: user.id }
+ }
+ );
+
+ expect(companyAssociation.notifications).toEqual([]);
+
+ const { mutate } = makeClient(user);
+
+ const newNotifications = [UserNotification.REVISION_REQUEST];
+
+ const { errors } = await mutate<
+ Pick,
+ MutationSetCompanyNotificationsArgs
+ >(SET_COMPANY_NOTIFICATIONS, {
+ variables: {
+ input: {
+ companyOrgId: company.orgId,
+ notifications: newNotifications
+ }
+ }
+ });
+
+ expect(errors).toEqual([
+ expect.objectContaining({
+ message:
+ "Votre rôle au sein de l'établissement ne vous permet pas de recevoir les notifications de type REVISION_REQUEST"
+ })
+ ]);
+ });
+});
diff --git a/back/src/users/resolvers/mutations/acceptMembershipRequest.ts b/back/src/users/resolvers/mutations/acceptMembershipRequest.ts
index 94ce2f6def..581bef7738 100644
--- a/back/src/users/resolvers/mutations/acceptMembershipRequest.ts
+++ b/back/src/users/resolvers/mutations/acceptMembershipRequest.ts
@@ -11,10 +11,10 @@ import {
MembershipRequestAlreadyAccepted,
MembershipRequestAlreadyRefused
} from "../../errors";
-import { convertUrls } from "../../../companies/database";
import { membershipRequestAccepted, renderMail } from "@td/mail";
import { checkUserPermissions, Permission } from "../../../permissions";
import { NotCompanyAdminErrorMsg } from "../../../common/errors";
+import { toGqlCompanyPrivate } from "../../../companies/converters";
const acceptMembershipRequestResolver: MutationResolvers["acceptMembershipRequest"] =
async (_, { id, role }, context) => {
@@ -84,7 +84,7 @@ const acceptMembershipRequestResolver: MutationResolvers["acceptMembershipReques
const dbCompany = await prisma.company.findUnique({
where: { id: company.id }
});
- return convertUrls(dbCompany!);
+ return toGqlCompanyPrivate(dbCompany!);
};
export default acceptMembershipRequestResolver;
diff --git a/back/src/users/resolvers/mutations/changeUserRole.ts b/back/src/users/resolvers/mutations/changeUserRole.ts
index 94005f7982..b63c8c6302 100644
--- a/back/src/users/resolvers/mutations/changeUserRole.ts
+++ b/back/src/users/resolvers/mutations/changeUserRole.ts
@@ -25,6 +25,7 @@ import {
updateCompanyAssociation,
updateUserAccountHash
} from "../../database";
+import { ALL_NOTIFICATIONS } from "../../notifications";
const changeUserRoleResolver: MutationResolvers["changeUserRole"] = async (
parent,
@@ -46,12 +47,24 @@ const changeUserRoleResolver: MutationResolvers["changeUserRole"] = async (
where: { company: { orgId: args.orgId }, userId: args.userId }
});
if (association) {
+ if (args.role === association.role) {
+ // Évite de faire l'update si le rôle est inchangé
+ return userAssociationToCompanyMember(
+ { ...association, user },
+ company.orgId,
+ user.id,
+ isTDAdmin
+ );
+ }
+
const updatedAssociation = await updateCompanyAssociation({
associationId: association.id,
data: {
- role: args.role
+ role: args.role,
+ notifications: args.role === "ADMIN" ? ALL_NOTIFICATIONS : []
}
});
+
if (!updatedAssociation) {
throw new UserInputError(
`L'utilisateur n'est pas membre de l'entreprise`
diff --git a/back/src/users/resolvers/mutations/deleteInvitation.ts b/back/src/users/resolvers/mutations/deleteInvitation.ts
index dddb9650b1..6265ad2d9a 100644
--- a/back/src/users/resolvers/mutations/deleteInvitation.ts
+++ b/back/src/users/resolvers/mutations/deleteInvitation.ts
@@ -1,10 +1,7 @@
import { prisma } from "@td/prisma";
import { applyAuthStrategies, AuthType } from "../../../auth";
import { checkIsAuthenticated } from "../../../common/permissions";
-import {
- convertUrls,
- getCompanyOrCompanyNotFound
-} from "../../../companies/database";
+import { getCompanyOrCompanyNotFound } from "../../../companies/database";
import { MutationResolvers } from "../../../generated/graphql/types";
import { getUserAccountHashOrNotFound } from "../../database";
import {
@@ -12,6 +9,7 @@ import {
Permission
} from "../../../permissions";
import { NotCompanyAdminErrorMsg } from "../../../common/errors";
+import { toGqlCompanyPrivate } from "../../../companies/converters";
const deleteInvitationResolver: MutationResolvers["deleteInvitation"] = async (
parent,
@@ -36,7 +34,7 @@ const deleteInvitationResolver: MutationResolvers["deleteInvitation"] = async (
const dbCompany = await prisma.company.findUnique({
where: { orgId: siret }
});
- return convertUrls(dbCompany!);
+ return toGqlCompanyPrivate(dbCompany!);
};
export default deleteInvitationResolver;
diff --git a/back/src/users/resolvers/mutations/inviteUserToCompanyService.ts b/back/src/users/resolvers/mutations/inviteUserToCompanyService.ts
index c0f599e50c..aa35dd7161 100644
--- a/back/src/users/resolvers/mutations/inviteUserToCompanyService.ts
+++ b/back/src/users/resolvers/mutations/inviteUserToCompanyService.ts
@@ -1,11 +1,6 @@
import { prisma } from "@td/prisma";
-
import { sendMail } from "../../../mailer/mailing";
-
-import {
- convertUrls,
- getCompanyOrCompanyNotFound
-} from "../../../companies/database";
+import { getCompanyOrCompanyNotFound } from "../../../companies/database";
import {
CompanyPrivate,
MutationInviteUserToCompanyArgs
@@ -15,6 +10,7 @@ import { associateUserToCompany, createUserAccountHash } from "../../database";
import { inviteUserToJoin, notifyUserOfInvite, renderMail } from "@td/mail";
import { User } from "@prisma/client";
+import { toGqlCompanyPrivate } from "../../../companies/converters";
export async function inviteUserToCompanyFn(
user: User,
@@ -68,5 +64,5 @@ export async function inviteUserToCompanyFn(
await sendMail(mail);
}
- return convertUrls(company);
+ return toGqlCompanyPrivate(company);
}
diff --git a/back/src/users/resolvers/mutations/refuseMembershipRequest.ts b/back/src/users/resolvers/mutations/refuseMembershipRequest.ts
index ac02892dba..0bf001fb95 100644
--- a/back/src/users/resolvers/mutations/refuseMembershipRequest.ts
+++ b/back/src/users/resolvers/mutations/refuseMembershipRequest.ts
@@ -1,4 +1,3 @@
-import { convertUrls } from "../../../companies/database";
import { prisma } from "@td/prisma";
import { applyAuthStrategies, AuthType } from "../../../auth";
import { sendMail } from "../../../mailer/mailing";
@@ -12,6 +11,7 @@ import {
import { renderMail, membershipRequestRefused } from "@td/mail";
import { checkUserPermissions, Permission } from "../../../permissions";
import { NotCompanyAdminErrorMsg } from "../../../common/errors";
+import { toGqlCompanyPrivate } from "../../../companies/converters";
const refuseMembershipRequestResolver: MutationResolvers["refuseMembershipRequest"] =
async (parent, { id }, context) => {
@@ -74,7 +74,7 @@ const refuseMembershipRequestResolver: MutationResolvers["refuseMembershipReques
const dbCompany = await prisma.company.findUnique({
where: { id: company.id }
});
- return convertUrls(dbCompany!);
+ return toGqlCompanyPrivate(dbCompany!);
};
export default refuseMembershipRequestResolver;
diff --git a/back/src/users/resolvers/mutations/removeUserFromCompany.ts b/back/src/users/resolvers/mutations/removeUserFromCompany.ts
index 88bd241848..c0b52875b7 100644
--- a/back/src/users/resolvers/mutations/removeUserFromCompany.ts
+++ b/back/src/users/resolvers/mutations/removeUserFromCompany.ts
@@ -1,10 +1,7 @@
import { prisma } from "@td/prisma";
import { applyAuthStrategies, AuthType } from "../../../auth";
import { checkIsAuthenticated } from "../../../common/permissions";
-import {
- convertUrls,
- getCompanyOrCompanyNotFound
-} from "../../../companies/database";
+import { getCompanyOrCompanyNotFound } from "../../../companies/database";
import { MutationResolvers } from "../../../generated/graphql/types";
import { getCompanyAssociationOrNotFound } from "../../database";
import { deleteCachedUserRoles } from "../../../common/redis/users";
@@ -13,6 +10,7 @@ import {
Permission
} from "../../../permissions";
import { NotCompanyAdminErrorMsg } from "../../../common/errors";
+import { toGqlCompanyPrivate } from "../../../companies/converters";
const removeUserFromCompanyResolver: MutationResolvers["removeUserFromCompany"] =
async (parent, { userId, siret }, context) => {
@@ -40,7 +38,7 @@ const removeUserFromCompanyResolver: MutationResolvers["removeUserFromCompany"]
where: { orgId: siret }
});
- return convertUrls(dbCompany!);
+ return toGqlCompanyPrivate(dbCompany!);
};
export default removeUserFromCompanyResolver;
diff --git a/back/src/users/resolvers/mutations/sendMembershipRequest.ts b/back/src/users/resolvers/mutations/sendMembershipRequest.ts
index 97e01b2b35..cc347b3f3b 100644
--- a/back/src/users/resolvers/mutations/sendMembershipRequest.ts
+++ b/back/src/users/resolvers/mutations/sendMembershipRequest.ts
@@ -14,6 +14,8 @@ import {
} from "@td/mail";
import { getEmailDomain, canSeeEmail } from "../../utils";
import { UserInputError } from "../../../common/errors";
+import { getNotificationSubscribers } from "../../notifications";
+import { UserNotification } from "@prisma/client";
const sendMembershipRequestResolver: MutationResolvers["sendMembershipRequest"] =
async (parent, { siret }, context) => {
@@ -49,31 +51,33 @@ const sendMembershipRequestResolver: MutationResolvers["sendMembershipRequest"]
);
}
- const emails = admins.map(a => a.email);
+ const subscribers = await getNotificationSubscribers(
+ UserNotification.MEMBERSHIP_REQUEST,
+ [siret]
+ );
const membershipRequest = await prisma.membershipRequest.create({
data: {
user: { connect: { id: user.id } },
company: { connect: { id: company.id } },
- sentTo: emails
+ sentTo: subscribers.map(r => r.email)
}
});
- // send membership request to all admins of the company
- const recipients = admins.map(a => ({ email: a.email, name: a.name! }));
-
- await sendMail(
- renderMail(membershipRequestMail, {
- to: recipients,
- variables: {
- userEmail: user.email,
- companyName: company.name,
- companySiret: company.orgId,
- companyGivenName: company.givenName,
- membershipRequestId: membershipRequest.id
- }
- })
- );
+ if (subscribers) {
+ await sendMail(
+ renderMail(membershipRequestMail, {
+ to: subscribers,
+ variables: {
+ userEmail: user.email,
+ companyName: company.name,
+ companySiret: company.orgId,
+ companyGivenName: company.givenName,
+ membershipRequestId: membershipRequest.id
+ }
+ })
+ );
+ }
// send membership request confirmation to requester
// Iot let him/her know about admin emails, we filter them (same domain name, no public email providers)
diff --git a/back/src/users/resolvers/mutations/setCompanyNotifications.ts b/back/src/users/resolvers/mutations/setCompanyNotifications.ts
new file mode 100644
index 0000000000..f0ceeac1a1
--- /dev/null
+++ b/back/src/users/resolvers/mutations/setCompanyNotifications.ts
@@ -0,0 +1,50 @@
+import { GraphQLContext } from "../../../types";
+import { applyAuthStrategies, AuthType } from "../../../auth";
+import { NotCompanyMember, UserInputError } from "../../../common/errors";
+
+import { checkIsAuthenticated } from "../../../common/permissions";
+import { getCompanyOrCompanyNotFound } from "../../../companies/database";
+import { MutationResolvers } from "../../../generated/graphql/types";
+import { prisma } from "@td/prisma";
+import { toGqlCompanyPrivate } from "../../../companies/converters";
+import { authorizedNotifications } from "../../notifications";
+
+const setCompanyNotificationsResolver: MutationResolvers["setCompanyNotifications"] =
+ async (parent, args, context: GraphQLContext) => {
+ applyAuthStrategies(context, [AuthType.Session]);
+
+ const { companyOrgId, notifications } = args.input;
+
+ const user = checkIsAuthenticated(context);
+
+ const company = await getCompanyOrCompanyNotFound({ orgId: companyOrgId });
+
+ const companyAssociation = await prisma.companyAssociation.findFirst({
+ where: { companyId: company.id, userId: user.id }
+ });
+
+ if (!companyAssociation) {
+ throw new NotCompanyMember(company.orgId);
+ }
+
+ const unauthorizedNotifications = notifications.filter(
+ notification =>
+ !authorizedNotifications[companyAssociation.role].includes(notification)
+ );
+
+ if (unauthorizedNotifications.length) {
+ throw new UserInputError(
+ "Votre rôle au sein de l'établissement ne vous permet pas de recevoir " +
+ `les notifications de type ${unauthorizedNotifications.join(", ")}`
+ );
+ }
+
+ const updatedCompanyAssociation = await prisma.companyAssociation.update({
+ where: { id: companyAssociation.id },
+ data: { notifications },
+ include: { company: true }
+ });
+
+ return toGqlCompanyPrivate(updatedCompanyAssociation.company);
+ };
+export default setCompanyNotificationsResolver;
diff --git a/back/src/users/resolvers/queries/__tests__/myCompanies.integration.ts b/back/src/users/resolvers/queries/__tests__/myCompanies.integration.ts
index 933a0fd82b..25039c34e2 100644
--- a/back/src/users/resolvers/queries/__tests__/myCompanies.integration.ts
+++ b/back/src/users/resolvers/queries/__tests__/myCompanies.integration.ts
@@ -38,6 +38,7 @@ const MY_COMPANIES = gql`
cursor
node {
id
+ orgId
siret
givenName
name
@@ -47,6 +48,7 @@ const MY_COMPANIES = gql`
}
userRole
userPermissions
+ userNotifications
}
}
}
@@ -462,4 +464,43 @@ describe("query { myCompanies }", () => {
"BSD_CAN_REVISE"
]);
});
+
+ it("should return userNotifications", async () => {
+ const user = await userFactory();
+ const company1 = await companyFactory();
+ const company2 = await companyFactory();
+ const company3 = await companyFactory();
+
+ await associateUserToCompany(user.id, company1.orgId, "ADMIN", {
+ notifications: ["MEMBERSHIP_REQUEST"]
+ });
+ await associateUserToCompany(user.id, company2.orgId, "ADMIN", {
+ notifications: ["BSD_REFUSAL"]
+ });
+ await associateUserToCompany(user.id, company3.orgId, "ADMIN", {
+ notifications: ["SIGNATURE_CODE_RENEWAL", "REVISION_REQUEST"]
+ });
+ const { query } = makeClient(user);
+ const { data } = await query>(MY_COMPANIES);
+ expect(data.myCompanies.edges).toHaveLength(3);
+
+ const userNotificationsByOrgId = data.myCompanies.edges.reduce(
+ (notifications, edge) => {
+ return {
+ ...notifications,
+ [edge.node.orgId]: edge.node.userNotifications
+ };
+ },
+ {}
+ );
+
+ expect(userNotificationsByOrgId[company1.orgId]).toEqual([
+ "MEMBERSHIP_REQUEST"
+ ]);
+ expect(userNotificationsByOrgId[company2.orgId]).toEqual(["BSD_REFUSAL"]);
+ expect(userNotificationsByOrgId[company3.orgId]).toEqual([
+ "SIGNATURE_CODE_RENEWAL",
+ "REVISION_REQUEST"
+ ]);
+ });
});
diff --git a/back/src/users/resolvers/queries/myCompanies.ts b/back/src/users/resolvers/queries/myCompanies.ts
index 80a688f915..a24e93cb11 100644
--- a/back/src/users/resolvers/queries/myCompanies.ts
+++ b/back/src/users/resolvers/queries/myCompanies.ts
@@ -1,19 +1,16 @@
import { Company } from "@prisma/client";
import { getConnection } from "../../../common/pagination";
import { checkIsAuthenticated } from "../../../common/permissions";
-import { convertUrls } from "../../../companies/database";
-import {
- CompanyPrivate,
- QueryResolvers
-} from "../../../generated/graphql/types";
+import { QueryResolvers } from "../../../generated/graphql/types";
import { prisma } from "@td/prisma";
import { AuthType } from "../../../auth";
import {
MIN_MY_COMPANIES_SEARCH,
MAX_MY_COMPANIES_SEARCH
} from "@td/constants";
+
+import { toGqlCompanyPrivate } from "../../../companies/converters";
import { UserInputError } from "../../../common/errors";
-import { libelleFromCodeNaf } from "../../../companies/sirene/utils";
const myCompaniesResolver: QueryResolvers["myCompanies"] = async (
_parent,
@@ -85,12 +82,7 @@ const myCompaniesResolver: QueryResolvers["myCompanies"] = async (
}
]
}),
- formatNode: (company: Company) => {
- const companyPrivate: CompanyPrivate = convertUrls(company);
- const { codeNaf: naf, address } = company;
- const libelleNaf = libelleFromCodeNaf(naf!);
- return { ...companyPrivate, naf, libelleNaf, address };
- },
+ formatNode: (company: Company) => toGqlCompanyPrivate(company),
...paginationArgs
});
};
diff --git a/back/src/users/typeDefs/private/user.inputs.graphql b/back/src/users/typeDefs/private/user.inputs.graphql
index 27f6f09ceb..6fb05a1a76 100644
--- a/back/src/users/typeDefs/private/user.inputs.graphql
+++ b/back/src/users/typeDefs/private/user.inputs.graphql
@@ -34,3 +34,13 @@ input CreatePasswordResetRequestInput {
email: String!
captcha: CaptchaInput!
}
+
+input SetCompanyNotificationsInput {
+ "Identifiant de l'établissement"
+ companyOrgId: String!
+ """
+ Notifications auxquelles l'utilisateur souhaite être abonné pour l'établisement
+ identifié par le paramètre companyOrgId
+ """
+ notifications: [UserNotification!]!
+}
diff --git a/back/src/users/typeDefs/private/user.mutations.graphql b/back/src/users/typeDefs/private/user.mutations.graphql
index 7c8bae3c36..8df876d8f7 100644
--- a/back/src/users/typeDefs/private/user.mutations.graphql
+++ b/back/src/users/typeDefs/private/user.mutations.graphql
@@ -117,4 +117,11 @@ type Mutation {
Change le rôle d'un utilisateur de l'entreprise
"""
changeUserRole(userId: ID!, orgId: ID!, role: UserRole!): CompanyMember!
+
+ """
+ USAGE INTERNE
+ Modifie les préfèrences d'abonnement de l'utilisateur connecté aux notifications
+ d'un établissement donné
+ """
+ setCompanyNotifications(input: SetCompanyNotificationsInput!): CompanyPrivate!
}
diff --git a/back/src/users/typeDefs/user.enums.graphql b/back/src/users/typeDefs/user.enums.graphql
index 8b70db73d6..89f2a3e3b5 100644
--- a/back/src/users/typeDefs/user.enums.graphql
+++ b/back/src/users/typeDefs/user.enums.graphql
@@ -27,6 +27,24 @@ enum UserRole {
ADMIN
}
+"""
+Notifications auxquelles un utilisateur peut s'abonner pour un établissement donné.
+Par défaut les utilisateurs avec le rôle ADMIN sont abonnés à toutes les notifications
+tandis que les autres rôles ne sont abonnés à aucune notification.
+"""
+enum UserNotification {
+ # Notification de demande de rattachement
+ MEMBERSHIP_REQUEST
+ # Notification de renouvellement du code signature
+ SIGNATURE_CODE_RENEWAL
+ # Notifications en cas de refus total ou partiel d'un BSD
+ BSD_REFUSAL
+ # Notification lors de la modification de la destination finale amiante
+ BSDA_FINAL_DESTINATION_UPDATE
+ # Notification lors d'une demande de révision
+ REVISION_REQUEST
+}
+
enum UserPermission {
"Lire un BSD."
BSD_CAN_READ
@@ -66,6 +84,8 @@ enum UserPermission {
COMPANY_CAN_MANAGE_MEMBERS
"Renouveler le code de signature"
COMPANY_CAN_RENEW_SECURITY_CODE
+ "Créer ou révoquer une délégation pour le registre terres & sédiments"
+ COMPANY_CAN_MANAGE_REGISTRY_DELEAGATION
}
"""
diff --git a/back/src/utils.ts b/back/src/utils.ts
index b0f9415ee1..405c6819de 100644
--- a/back/src/utils.ts
+++ b/back/src/utils.ts
@@ -58,6 +58,28 @@ export function sameDayMidnight(date: Date): Date {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
+export function startOfDay(date: Date | string): Date {
+ const result = new Date(date);
+ result.setHours(0, 0, 0, 0);
+ return result;
+}
+
+export function endOfDay(date: Date | string): Date {
+ const result = new Date(date);
+ result.setHours(23, 59, 59, 999);
+ return result;
+}
+
+export function todayAtMidnight(): Date {
+ return sameDayMidnight(new Date());
+}
+
+export const nowPlusXHours = (hours: number) => {
+ const res = new Date();
+ res.setHours(res.getHours() + hours);
+ return res;
+};
+
/**
* Number of days between two dates
*/
diff --git a/e2e/src/data/company.ts b/e2e/src/data/company.ts
index 8487f8364a..63b35da7be 100644
--- a/e2e/src/data/company.ts
+++ b/e2e/src/data/company.ts
@@ -1,6 +1,6 @@
import { prisma } from "@td/prisma";
import { generateUniqueTestSiret, randomNbrChain } from "back";
-import { CompanyType, Prisma } from "@prisma/client";
+import { CompanyType, Prisma, WasteVehiclesType } from "@prisma/client";
interface VhuAgrement {
agrementNumber: string;
@@ -162,6 +162,10 @@ export const seedDefaultCompanies = async () => {
{
name: "I - Broyeur / casse automobile",
companyTypes: [CompanyType.WASTE_VEHICLES],
+ wasteVehiclesTypes: [
+ WasteVehiclesType.BROYEUR,
+ WasteVehiclesType.DEMOLISSEUR
+ ],
contact: "Monsieur Démolisseur et Broyeur",
contactPhone: "0458758956",
contactEmail: "monsieurbroyeuretdemolisseur@gmail.com"
@@ -202,6 +206,7 @@ export const seedDefaultCompanies = async () => {
{
name: "N - Démolisseur",
companyTypes: [CompanyType.WASTE_VEHICLES],
+ wasteVehiclesTypes: [WasteVehiclesType.DEMOLISSEUR],
contact: "Monsieur Démolisseur",
contactPhone: "0473625689",
contactEmail: "monsieurdemolisseur@gmail.com"
@@ -218,6 +223,7 @@ export const seedDefaultCompanies = async () => {
{
name: "O - Broyeur",
companyTypes: [CompanyType.WASTE_VEHICLES],
+ wasteVehiclesTypes: [WasteVehiclesType.BROYEUR],
contact: "Monsieur Broyeur",
contactPhone: "0475875695",
contactEmail: "monsieurbroyeur@gmail.com"
diff --git a/e2e/src/utils/bsvhu.ts b/e2e/src/utils/bsvhu.ts
index 271add499d..423848c152 100644
--- a/e2e/src/utils/bsvhu.ts
+++ b/e2e/src/utils/bsvhu.ts
@@ -343,17 +343,21 @@ export const verifyCardData = async (
.locator(".actors__label")
.first()
.getAttribute("aria-label");
- await expect(emitterName).toEqual(emitter.name);
+ await expect(emitterName).toEqual(`Expédition du bordereau ${emitter.name}`);
const transporterName = await vhuDiv
.locator(".actors__label")
.nth(1)
.getAttribute("aria-label");
- await expect(transporterName).toEqual(transporter.name);
+ await expect(transporterName).toEqual(
+ `Transporteur visé sur le bordereau ${transporter.name}`
+ );
const destinationName = await vhuDiv
.locator(".actors__label")
.nth(2)
.getAttribute("aria-label");
- await expect(destinationName).toEqual(destination.name);
+ await expect(destinationName).toEqual(
+ `Destination du bordereau ${destination.name}`
+ );
// Primary button
await expect(vhuDiv.getByRole("button").getByText("Publier")).toBeVisible();
diff --git a/front/index.html b/front/index.html
index 3ba0e363eb..cc26891b78 100644
--- a/front/index.html
+++ b/front/index.html
@@ -2,50 +2,40 @@
+
+
+
+
+
-
-
-
-
-
-
-
-
-
Trackdéchets | La traçabilité des déchets en toute sécurité
diff --git a/front/project.json b/front/project.json
index 94a54d93df..c8514e4b05 100644
--- a/front/project.json
+++ b/front/project.json
@@ -7,7 +7,7 @@
"dsfr": {
"executor": "nx:run-commands",
"options": {
- "command": "cd front && copy-dsfr-to-public && only-include-used-icons"
+ "command": "cd front && react-dsfr update-icons && react-dsfr copy-static-assets"
}
},
"build": {
diff --git a/front/src/App.tsx b/front/src/App.tsx
index 41d51cc854..077a7965ba 100644
--- a/front/src/App.tsx
+++ b/front/src/App.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { ApolloProvider } from "@apollo/client";
-import { BrowserRouter as Router } from "react-router-dom";
+import { createBrowserRouter, RouterProvider } from "react-router-dom";
import client from "./graphql-client";
import LayoutContainer from "./Apps/common/Components/layout/LayoutContainer";
import setYupLocale from "./common/setYupLocale";
@@ -13,20 +13,22 @@ import { PermissionsProvider } from "./common/contexts/PermissionsContext";
// See https://github.com/jquense/yup#using-a-custom-locale-dictionary
setYupLocale();
+const router = createBrowserRouter([
+ { path: "*", element: }
+]);
+
export default function App() {
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/front/src/Apps/Account/Account.tsx b/front/src/Apps/Account/Account.tsx
index 46ed73c423..570b8887d0 100644
--- a/front/src/Apps/Account/Account.tsx
+++ b/front/src/Apps/Account/Account.tsx
@@ -14,6 +14,7 @@ import AccountApplications from "./AccountApplications/AccountApplications";
import "../Dashboard/dashboard.scss";
import { useMedia } from "../../common/use-media";
import { MEDIA_QUERIES } from "../../common/config";
+import AccountNotifications from "./AccountNotifications/AccountNotifications";
export const GET_ME = gql`
{
@@ -41,7 +42,7 @@ export default function Account() {
return (
{!isMobile &&
}
-
+
} />
@@ -54,6 +55,15 @@ export default function Account() {
}
/>
+
+
+
+ }
+ />
+
-
{title}
+
{title}
{subtitle && (
{subtitle}
diff --git a/front/src/Apps/Account/AccountInfo/AccountInfoActionBar.tsx b/front/src/Apps/Account/AccountInfo/AccountInfoActionBar.tsx
index 0330f9d1db..17e7f2bbe2 100644
--- a/front/src/Apps/Account/AccountInfo/AccountInfoActionBar.tsx
+++ b/front/src/Apps/Account/AccountInfo/AccountInfoActionBar.tsx
@@ -18,7 +18,7 @@ const AccountInfoActionBar = ({
isDisabled
}: AccountInfoActionBarProps) => (
- {title &&
{title} }
+ {title &&
{title} }
{!isEditing && (
diff --git a/front/src/Apps/Account/AccountMembershipRequest.tsx b/front/src/Apps/Account/AccountMembershipRequest.tsx
index f983899e9a..fbb0f535e7 100644
--- a/front/src/Apps/Account/AccountMembershipRequest.tsx
+++ b/front/src/Apps/Account/AccountMembershipRequest.tsx
@@ -117,9 +117,9 @@ export default function AccountMembershipRequest() {
return (
-
+
Un utilisateur aimerait rejoindre l'établissement {name} ({siret})
-
+
{email}
diff --git a/front/src/Apps/Account/AccountMenu.tsx b/front/src/Apps/Account/AccountMenu.tsx
index 784a790d94..2f9be3071f 100644
--- a/front/src/Apps/Account/AccountMenu.tsx
+++ b/front/src/Apps/Account/AccountMenu.tsx
@@ -6,7 +6,12 @@ import { Accordion } from "@codegouvfr/react-dsfr/Accordion";
export const AccountMenuContent = () => (
<>
-
+
(
Mes paramètres
+
+
+ isActive
+ ? "sidebarv2__item sidebarv2__item--indented sidebarv2__item--active"
+ : "sidebarv2__item sidebarv2__item--indented"
+ }
+ >
+ Notifications
+
+
-
+
+
+ {/* Liste paginée des établissements avec un bouton "Charger plus"
+ et une barre de recherche */}
+ (
+ [
+ ,
+ ,
+
+
+
+ ])}
+ headers={["Établissements", "Notifications", ""]}
+ />
+ )}
+ />
+
+ );
+}
diff --git a/front/src/Apps/Account/AccountNotifications/CompanyDisplay.module.scss b/front/src/Apps/Account/AccountNotifications/CompanyDisplay.module.scss
new file mode 100644
index 0000000000..ab8c74598a
--- /dev/null
+++ b/front/src/Apps/Account/AccountNotifications/CompanyDisplay.module.scss
@@ -0,0 +1,9 @@
+.companyName {
+ color: var(--text-action-high-blue-france);
+ font-weight: bold;
+}
+
+.roleTag {
+ color: var(--text-action-high-blue-france);
+ background-color: var(--background-action-low-blue-france);
+}
diff --git a/front/src/Apps/Account/AccountNotifications/CompanyDisplay.tsx b/front/src/Apps/Account/AccountNotifications/CompanyDisplay.tsx
new file mode 100644
index 0000000000..34ee346724
--- /dev/null
+++ b/front/src/Apps/Account/AccountNotifications/CompanyDisplay.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import { CompanyPrivate } from "@td/codegen-ui";
+import Tag from "@codegouvfr/react-dsfr/Tag";
+import { userRoleLabel } from "../../Companies/common/utils";
+import styles from "./CompanyDisplay.module.scss";
+
+type AccountCompanyNotificationsProps = {
+ company: CompanyPrivate;
+};
+
+export default function CompanyDisplay({
+ company
+}: AccountCompanyNotificationsProps) {
+ return (
+ <>
+
+ {company.name}
+ {company.givenName && ` - ${company.givenName}`}
+
+ {company.orgId}
+
+ {userRoleLabel(company.userRole!)}
+
+ >
+ );
+}
diff --git a/front/src/Apps/Account/AccountNotifications/NotificationsDisplay.tsx b/front/src/Apps/Account/AccountNotifications/NotificationsDisplay.tsx
new file mode 100644
index 0000000000..2fbfdce5a0
--- /dev/null
+++ b/front/src/Apps/Account/AccountNotifications/NotificationsDisplay.tsx
@@ -0,0 +1,27 @@
+import { CompanyPrivate, UserNotification } from "@td/codegen-ui";
+import React from "react";
+
+type AccountCompanyNotificationsProps = {
+ company: CompanyPrivate;
+};
+
+const notificationLabels: { [key in UserNotification]: string } = {
+ [UserNotification.MembershipRequest]: "Rattachement",
+ [UserNotification.SignatureCodeRenewal]: "Code signature",
+ [UserNotification.BsdRefusal]: "Refus",
+ [UserNotification.BsdaFinalDestinationUpdate]: "Destination finale amiante",
+ [UserNotification.RevisionRequest]: "Révision"
+};
+
+export function NotificationsDisplay({
+ company
+}: AccountCompanyNotificationsProps) {
+ if (!company.userNotifications || company.userNotifications.length === 0) {
+ return Désactivé
;
+ }
+ return (
+
+ {company.userNotifications.map(n => notificationLabels[n]).join(", ")}
+
+ );
+}
diff --git a/front/src/Apps/Account/AccountNotifications/NotificationsUpdateAllButton.tsx b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateAllButton.tsx
new file mode 100644
index 0000000000..71c7a9aab5
--- /dev/null
+++ b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateAllButton.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import Button from "@codegouvfr/react-dsfr/Button";
+import { createModal } from "@codegouvfr/react-dsfr/Modal";
+import AccountNotificationsUpdateAllModal from "./NotificationsUpdateAllModal";
+
+type AccountNotificationsUpdateAllButtonProps = {
+ // nombre total d'établissements
+ totalCount: number;
+};
+
+export default function NotificationsUpdateAllButton({
+ totalCount
+}: AccountNotificationsUpdateAllButtonProps) {
+ const modal = createModal({
+ id: `update-all-notifications`,
+ isOpenedByDefault: false
+ });
+
+ return (
+ <>
+
+
+
+ modal.open()}>
+ Gérer les notifications en masse
+
+ >
+ );
+}
diff --git a/front/src/Apps/Account/AccountNotifications/NotificationsUpdateAllModal.tsx b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateAllModal.tsx
new file mode 100644
index 0000000000..b6b650418a
--- /dev/null
+++ b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateAllModal.tsx
@@ -0,0 +1,17 @@
+import React from "react";
+
+type AccountNotificationsUpdateAllModalProps = {
+ // nombre total d'établissements
+ totalCount: number;
+};
+
+export default function AccountNotificationsUpdateAllModal({
+ totalCount
+}: AccountNotificationsUpdateAllModalProps) {
+ return (
+
+ {/* TODO implémenter les actions en masse dans cette modale */}
+ Mise à jour en masse de {totalCount} établissements
+
+ );
+}
diff --git a/front/src/Apps/Account/AccountNotifications/NotificationsUpdateButton.tsx b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateButton.tsx
new file mode 100644
index 0000000000..8c87573d93
--- /dev/null
+++ b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateButton.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import Button from "@codegouvfr/react-dsfr/Button";
+import { createModal } from "@codegouvfr/react-dsfr/Modal";
+import { CompanyPrivate } from "@td/codegen-ui";
+import NotificationsUpdateModal from "./NotificationsUpdateModal";
+
+type AccountCompanyNotificationsUpdateButtonProps = {
+ company: CompanyPrivate;
+};
+
+export default function NotificationsUpdateButton({
+ company
+}: AccountCompanyNotificationsUpdateButtonProps) {
+ const btnLabel = `Gérer (${company.userNotifications.length})`;
+
+ const modalTitle = `Gérer les notifications`;
+
+ const iconId = company.userNotifications?.length
+ ? "ri-notification-3-line"
+ : "ri-notification-off-line";
+
+ const modal = createModal({
+ id: `${company.orgId}-notifications-update`,
+ isOpenedByDefault: false
+ });
+
+ return (
+ <>
+
+
+
+ modal.open()} iconId={iconId}>
+ {btnLabel}
+
+ >
+ );
+}
diff --git a/front/src/Apps/Account/AccountNotifications/NotificationsUpdateModal.module.scss b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateModal.module.scss
new file mode 100644
index 0000000000..fb8b86ec5e
--- /dev/null
+++ b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateModal.module.scss
@@ -0,0 +1,5 @@
+.buttons {
+ display: flex;
+ justify-content: right;
+ gap: 15px;
+}
diff --git a/front/src/Apps/Account/AccountNotifications/NotificationsUpdateModal.tsx b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateModal.tsx
new file mode 100644
index 0000000000..e4f854f6a4
--- /dev/null
+++ b/front/src/Apps/Account/AccountNotifications/NotificationsUpdateModal.tsx
@@ -0,0 +1,153 @@
+import React from "react";
+import {
+ CompanyPrivate,
+ Mutation,
+ MutationSetCompanyNotificationsArgs,
+ UserNotification
+} from "@td/codegen-ui";
+import { useForm } from "react-hook-form";
+import Checkbox from "@codegouvfr/react-dsfr/Checkbox";
+import Button from "@codegouvfr/react-dsfr/Button";
+import { useMutation } from "@apollo/client";
+import { SET_COMPANY_NOTIFICATIONS } from "./queries";
+import styles from "./NotificationsUpdateModal.module.scss";
+import {
+ ALL_NOTIFICATIONS,
+ authorizedNotifications as authorizedNotificationsByUserRole
+} from "../../../common/notifications";
+
+type AccountCompanyNotificationsUpdateModalProps = {
+ company: CompanyPrivate;
+ close: () => void;
+};
+
+type FormValues = { [key in UserNotification]: boolean };
+
+export default function NotificationsUpdateModal({
+ company,
+ close
+}: AccountCompanyNotificationsUpdateModalProps) {
+ const [setCompanyNotifications, { loading, data, error }] = useMutation<
+ Pick,
+ MutationSetCompanyNotificationsArgs
+ >(SET_COMPANY_NOTIFICATIONS);
+
+ const defaultValues: FormValues = ALL_NOTIFICATIONS.reduce(
+ (values, notification) => ({
+ ...values,
+ [notification]: company.userNotifications.includes(notification)
+ }),
+ {} as FormValues
+ );
+
+ const authorizedNotifications = company.userRole
+ ? authorizedNotificationsByUserRole[company.userRole]
+ : [];
+
+ const { register, handleSubmit } = useForm({
+ defaultValues
+ });
+
+ const onSubmit = async (data: FormValues) => {
+ const notifications = Object.keys(data).filter(k => data[k]);
+ const { errors } = await setCompanyNotifications({
+ variables: {
+ input: {
+ companyOrgId: company.orgId,
+ notifications: notifications as UserNotification[]
+ }
+ }
+ });
+ if (!errors) {
+ close();
+ }
+ };
+
+ let checkboxState: "default" | "error" | "success" = "default";
+
+ if (error) {
+ checkboxState = "error";
+ } else if (data) {
+ checkboxState = "success";
+ }
+
+ const optionsLabels = [
+ {
+ hintText:
+ "Seuls les membres avec le rôle Administrateur sont en mesure de recevoir " +
+ "et d'accepter / refuser / effectuer des demandes de rattachement à leur établissement. " +
+ "Nous vous conseillons donc vivement, pour chaque établissement de conserver au moins un " +
+ "administrateur abonné à ce type de notification.",
+ label: "aux demandes de rattachement",
+ notification: UserNotification.MembershipRequest
+ },
+ {
+ hintText:
+ "Un courriel sera envoyé à chaque renouvellement du code de signature",
+ label: "au renouvellement du code de signature",
+ notification: UserNotification.SignatureCodeRenewal
+ },
+ {
+ hintText:
+ "un courriel sera envoyé à chaque refus total ou partiel d'un bordereau",
+ label: "au refus total et partiel des bordereaux",
+ notification: UserNotification.BsdRefusal
+ },
+ {
+ hintText:
+ "Un courriel sera envoyé lorsque le BSDA est envoyé à un exutoire" +
+ " différent de celui prévu lors de la signature producteur",
+ label: "à la modification de la destination finale amiante",
+ notification: UserNotification.BsdaFinalDestinationUpdate
+ },
+ {
+ hintText:
+ "Un courriel sera envoyé à chaque fois qu'une révision sera restée sans réponse 14 jours après sa demande",
+ label: "aux demandes de révision",
+ notification: UserNotification.RevisionRequest
+ }
+ ];
+
+ const options = optionsLabels
+ .filter(({ notification }) =>
+ authorizedNotifications.includes(notification)
+ )
+ .map(({ label, hintText, notification }) => ({
+ label,
+ hintText,
+ nativeInputProps: {
+ ...register(notification)
+ }
+ }));
+
+ return (
+ <>
+
+ Je souhaite recevoir par courriel les notifications de l'établissement{" "}
+ {company.name} ({company.siret}) relatives :
+
+
+ >
+ );
+}
diff --git a/front/src/Apps/Account/AccountNotifications/queries.ts b/front/src/Apps/Account/AccountNotifications/queries.ts
new file mode 100644
index 0000000000..a8aa8bec31
--- /dev/null
+++ b/front/src/Apps/Account/AccountNotifications/queries.ts
@@ -0,0 +1,11 @@
+import gql from "graphql-tag";
+
+export const SET_COMPANY_NOTIFICATIONS = gql`
+ mutation SetCompanyNotifications($input: SetCompanyNotificationsInput!) {
+ setCompanyNotifications(input: $input) {
+ id
+ orgId
+ userNotifications
+ }
+ }
+`;
diff --git a/front/src/Apps/Companies/CompaniesList/CompaniesList.tsx b/front/src/Apps/Companies/CompaniesList/CompaniesList.tsx
index 237fef45b0..e1279f3082 100644
--- a/front/src/Apps/Companies/CompaniesList/CompaniesList.tsx
+++ b/front/src/Apps/Companies/CompaniesList/CompaniesList.tsx
@@ -1,28 +1,19 @@
-import React, { useState, useMemo, useEffect } from "react";
-import { useQuery } from "@apollo/client";
+import React, { useState } from "react";
import { Link, useNavigate, generatePath } from "react-router-dom";
-import { Query, CompanyPrivate, UserRole } from "@td/codegen-ui";
-import { Loader } from "../../common/Components";
-import { NotificationError } from "../../common/Components/Error/Error";
-import routes from "../../routes";
-import { debounce } from "../../../common/helper";
import {
- MIN_MY_COMPANIES_SEARCH,
- MAX_MY_COMPANIES_SEARCH
-} from "@td/constants";
+ CompanyPrivate,
+ UserRole,
+ CompanyPrivateConnection
+} from "@td/codegen-ui";
+import routes from "../../routes";
import styles from "./CompaniesList.module.scss";
import AccountContentWrapper from "../../Account/AccountContentWrapper";
-import { MY_COMPANIES } from "../common/queries";
import DropdownMenu from "../../common/Components/DropdownMenu/DropdownMenu";
import {
useDownloadMyCompaniesCsv,
useDownloadMyCompaniesXls
} from "../common/hooks/useDownloadCompanies";
-
-// Prevent to short and long clues
-const isSearchClueValid = clue =>
- clue.length >= MIN_MY_COMPANIES_SEARCH &&
- clue.length <= MAX_MY_COMPANIES_SEARCH;
+import SearchableCompaniesList from "./SearchableCompaniesList";
export const userRole = (role: UserRole) => {
let icon = "fr-icon-user-line";
@@ -53,88 +44,76 @@ export const userRole = (role: UserRole) => {
};
export default function CompaniesList() {
- const [isFiltered, setIsFiltered] = useState(false);
- const [inputValue, setInputValue] = useState("");
- const [searchClue, setSearchClue] = useState("");
- const [displayPreloader, setDisplayPreloader] = useState(false);
- const [userCompaniesTotalCount, setUserCompaniesTotalCount] = useState(0);
-
const navigate = useNavigate();
-
- const { data, loading, error, refetch, fetchMore } = useQuery<
- Pick
- >(MY_COMPANIES, { fetchPolicy: "network-only", variables: { first: 10 } });
-
- const debouncedSearch = useMemo(
- () =>
- debounce(params => {
- setSearchClue(params.search);
- refetch(params).then(() => setDisplayPreloader(false));
- }, 1000),
- [refetch, setDisplayPreloader]
+ const [downloadMyCompaniesCsv] = useDownloadMyCompaniesCsv();
+ const [downloadMyCompaniesXls] = useDownloadMyCompaniesXls();
+ const [companiesTotalCount, setCompaniesTotalCount] = useState(
+ null
);
- const handleOnChangeSearch = newSearchClue => {
- if (isSearchClueValid(newSearchClue)) {
- setDisplayPreloader(true);
- setIsFiltered(true);
- debouncedSearch({
- search: newSearchClue
- });
- }
+ const pluralize = count => (count > 1 ? "s" : "");
- if (newSearchClue.length === 0) {
- handleOnResetSearch();
+ const onMyCompaniesQueryCompleted = (
+ data: CompanyPrivateConnection,
+ isFiltered: boolean
+ ) => {
+ if (!isFiltered) {
+ if (data.totalCount === 0) {
+ // No results and we're not filtering, redirect to the create company screen
+ navigate(routes.companies.orientation);
+ }
+ setCompaniesTotalCount(data.totalCount);
}
};
- const handleOnResetSearch = () => {
- setInputValue("");
- setDisplayPreloader(true);
- setIsFiltered(false);
- debouncedSearch({ search: "" });
+ const renderCompanies = (companies: CompanyPrivate[]) => {
+ return (
+
+ {companies.map(company => (
+
+
+
+ {company.name}
+ {company.givenName && ` - ${company.givenName}`}
+
+
{company.orgId}
+
{company.address}
+
+ {userRole(company.userRole!)}
+
+
+
+
+ ))}
+
+ );
};
- useEffect(() => {
- if (data) {
- const companies = data.myCompanies?.edges.map(({ node }) => node);
- const totalCount = data.myCompanies?.totalCount ?? 0;
-
- setUserCompaniesTotalCount(previousTotalCount => {
- return previousTotalCount || totalCount;
- });
-
- if (!companies || (userCompaniesTotalCount === 0 && totalCount === 0)) {
- if (!isFiltered) {
- // No results and we're not filtering, redirect to the create company screen
- navigate(routes.companies.orientation);
- }
- }
- }
- }, [
- navigate,
- data,
- isFiltered,
- userCompaniesTotalCount,
- setUserCompaniesTotalCount
- ]);
-
- const pluralize = count => (count > 1 ? "s" : "");
- const listTitle = `Vous êtes membre de ${userCompaniesTotalCount} établissement${pluralize(
- userCompaniesTotalCount
- )}`;
-
- const [downloadMyCompaniesCsv] = useDownloadMyCompaniesCsv();
- const [downloadMyCompaniesXls] = useDownloadMyCompaniesXls();
+ const subtitle = companiesTotalCount
+ ? `Vous êtes membre de ${companiesTotalCount} établissement${pluralize(
+ companiesTotalCount
+ )}`
+ : "";
- const getPageContent = (content, subtitle?) => (
+ return (
- Créer un établissement
+ Créer un établissement
}
>
-
-
-
-
-
- Recherche
-
- {
- setInputValue(e.target.value);
- handleOnChangeSearch(e.target.value);
- }}
- value={inputValue}
- />
- (isFiltered ? handleOnResetSearch() : () => {})}
- >
- {isFiltered ? "Annuler" : "Rechercher"}
-
-
-
-
-
-
+
);
-
- if (error) {
- return ;
- }
-
- if (displayPreloader || loading) {
- return getPageContent( , listTitle);
- }
-
- if (data) {
- const companies = data.myCompanies?.edges.map(({ node }) => node);
- const totalCount = data.myCompanies?.totalCount ?? 0;
-
- const searchTitle = `${totalCount} résultat${pluralize(
- totalCount
- )} pour la recherche : ${searchClue}`;
-
- if (!companies || totalCount === 0) {
- if (isFiltered) {
- return getPageContent(
-
- Aucun résultat pour la recherche: {searchClue}
-
,
- listTitle
- );
- }
-
- return ;
- }
-
- const renderCompanies = companies => {
- return companies.map((company: CompanyPrivate) => (
-
-
-
- {company.name}
- {company.givenName && ` - ${company.givenName}`}
-
-
{company.orgId}
-
{company.address}
-
- {userRole(company.userRole!)}
-
-
-
-
- ));
- };
-
- return getPageContent(
-
- {isFiltered &&
{searchTitle}
}
-
{renderCompanies(companies)}
- {data.myCompanies?.pageInfo.hasNextPage && (
-
-
- fetchMore({
- variables: {
- first: 10,
- after: data.myCompanies?.pageInfo.endCursor
- }
- })
- }
- >
- Charger plus d'établissements
-
-
- )}
-
,
- listTitle
- );
- }
-
- return ;
}
diff --git a/front/src/Apps/Companies/CompaniesList/SearchableCompaniesList.tsx b/front/src/Apps/Companies/CompaniesList/SearchableCompaniesList.tsx
new file mode 100644
index 0000000000..d4d5e81941
--- /dev/null
+++ b/front/src/Apps/Companies/CompaniesList/SearchableCompaniesList.tsx
@@ -0,0 +1,192 @@
+import React, { useState, useMemo } from "react";
+import { useQuery } from "@apollo/client";
+import {
+ Query,
+ CompanyPrivate,
+ QueryMyCompaniesArgs,
+ CompanyPrivateConnection
+} from "@td/codegen-ui";
+import { Loader } from "../../common/Components";
+import { NotificationError } from "../../common/Components/Error/Error";
+
+import { debounce } from "../../../common/helper";
+import {
+ MIN_MY_COMPANIES_SEARCH,
+ MAX_MY_COMPANIES_SEARCH
+} from "@td/constants";
+import styles from "./CompaniesList.module.scss";
+
+import { MY_COMPANIES } from "../common/queries";
+
+type SearchableCompaniesListProps = {
+ renderCompanies: (
+ companies: CompanyPrivate[],
+ totalCount: number
+ ) => React.ReactNode;
+ // ocallback qui est appelé à chaque fois que l'on obtient une réponse
+ // de la query `myCompanies`
+ onCompleted?: (data: CompanyPrivateConnection, isFiltered: boolean) => void;
+};
+
+// Prevent too short and long clues
+const isSearchClueValid = clue =>
+ clue.length >= MIN_MY_COMPANIES_SEARCH &&
+ clue.length <= MAX_MY_COMPANIES_SEARCH;
+
+/**
+ * Composant abstrait permettant l'affichage d'une liste d'établissement
+ * paginé (par curseur de type "Charger plus") avec une barre de recherche.
+ */
+export default function SearchableCompaniesList({
+ renderCompanies,
+ onCompleted
+}: SearchableCompaniesListProps) {
+ const [isFiltered, setIsFiltered] = useState(false);
+ const [inputValue, setInputValue] = useState("");
+ const [searchClue, setSearchClue] = useState("");
+ const [displayPreloader, setDisplayPreloader] = useState(false);
+
+ const { data, loading, error, refetch, fetchMore } = useQuery<
+ Pick,
+ QueryMyCompaniesArgs
+ >(MY_COMPANIES, {
+ fetchPolicy: "network-only",
+ variables: { first: 10 },
+ onCompleted: data =>
+ onCompleted && onCompleted(data.myCompanies, isFiltered)
+ });
+
+ const debouncedSearch = useMemo(
+ () =>
+ debounce(params => {
+ setSearchClue(params.search);
+ refetch(params).then(() => setDisplayPreloader(false));
+ }, 1000),
+ [refetch, setDisplayPreloader]
+ );
+
+ const handleOnChangeSearch = newSearchClue => {
+ if (isSearchClueValid(newSearchClue)) {
+ setDisplayPreloader(true);
+ setIsFiltered(true);
+ debouncedSearch({
+ search: newSearchClue
+ });
+ }
+
+ if (newSearchClue.length === 0) {
+ handleOnResetSearch();
+ }
+ };
+
+ const handleOnResetSearch = () => {
+ setInputValue("");
+ setDisplayPreloader(true);
+ setIsFiltered(false);
+ debouncedSearch({ search: "" });
+ };
+
+ const pluralize = count => (count > 1 ? "s" : "");
+
+ const renderSearchableCompaniesList = (
+ myCompanies: CompanyPrivateConnection
+ ) => {
+ const companies = myCompanies.edges.map(({ node }) => node);
+ const totalCount = myCompanies.totalCount;
+ const searchTitle = `${totalCount} résultat${pluralize(
+ totalCount
+ )} pour la recherche : ${searchClue}`;
+
+ if (!companies || totalCount === 0) {
+ if (isFiltered) {
+ return (
+
+ Aucun résultat pour la recherche: {searchClue}
+
+ );
+ }
+ return ;
+ }
+
+ return (
+
+ {isFiltered &&
{searchTitle}
}
+ {/*
{companies.map(company => renderCompanies(company))}
*/}
+ {renderCompanies(companies, totalCount)}
+ {myCompanies.pageInfo.hasNextPage && (
+
+
+ fetchMore({
+ variables: {
+ first: 10,
+ after: myCompanies.pageInfo.endCursor
+ }
+ })
+ }
+ >
+ Charger plus d'établissements
+
+
+ )}
+
+ );
+ };
+
+ if (error) {
+ return ;
+ }
+
+ if (data) {
+ return (
+
+
+
+
+
+ Recherche
+
+ {
+ setInputValue(e.target.value);
+ handleOnChangeSearch(e.target.value);
+ }}
+ value={inputValue}
+ />
+ (isFiltered ? handleOnResetSearch() : () => {})}
+ >
+ {isFiltered ? "Annuler" : "Rechercher"}
+
+
+
+
+
+
+ {displayPreloader || loading ? (
+
+ ) : (
+ renderSearchableCompaniesList(data.myCompanies)
+ )}
+
+
+
+ );
+ }
+
+ return ;
+}
diff --git a/front/src/Apps/Companies/CompaniesMenu.tsx b/front/src/Apps/Companies/CompaniesMenu.tsx
index d259fa284d..eb93a697b0 100644
--- a/front/src/Apps/Companies/CompaniesMenu.tsx
+++ b/front/src/Apps/Companies/CompaniesMenu.tsx
@@ -5,7 +5,12 @@ import routes from "../routes";
import { Accordion } from "@codegouvfr/react-dsfr/Accordion";
export const CompaniesMenuContent = () => (
-
+
-
+
Réception d'un transfert administratif de bordereaux
-
+
Vous vous apprêtez à valider la modification des BSDs sur lesquels
l'établissement {administrativeTranfer.from.name} -{" "}
@@ -92,7 +92,7 @@ export function ApproveAdministrativeTransfer({ company }: Props) {
onClose={() => setIsModalOpened(false)}
isOpen={isModalOpened}
>
-
Valider le transfert
+ Valider le transfert
Êtes vous sûr de vouloir valider le transfert des BSDs sur lesquels
diff --git a/front/src/Apps/Companies/CompanyAdvanced/CompanyAdvanced.tsx b/front/src/Apps/Companies/CompanyAdvanced/CompanyAdvanced.tsx
index 0fd40d9d28..4e9da0aac3 100644
--- a/front/src/Apps/Companies/CompanyAdvanced/CompanyAdvanced.tsx
+++ b/front/src/Apps/Companies/CompanyAdvanced/CompanyAdvanced.tsx
@@ -90,9 +90,9 @@ const CompanyAdvanced = ({ company }: AdvancedProps) => {
return (
-
+
Mise en sommeil de l'établissement
-
+
La mise en sommeil de votre établissement ne permettra plus de viser
son SIRET sur de nouveaux bordereaux. Elle permettra de gérer les
@@ -142,9 +142,9 @@ const CompanyAdvanced = ({ company }: AdvancedProps) => {
-
+
Suppression de l'établissement
-
+
En supprimant cet établissement, vous supprimez les accès de tous ses
membres et vous ne pourrez plus accéder, ni au suivi des bordereaux, ni
@@ -164,9 +164,9 @@ const CompanyAdvanced = ({ company }: AdvancedProps) => {
onClose={onCloseModal}
isOpen={isModalOpened}
>
-
+
Supprimer {company?.name} - {company?.givenName}
-
+
Êtes vous sur de vouloir supprimer l'établissement {companyFullname} ?
diff --git a/front/src/Apps/Companies/CompanyAdvanced/RequestAdministrativeTransfer.tsx b/front/src/Apps/Companies/CompanyAdvanced/RequestAdministrativeTransfer.tsx
index 7595237dd7..c72472f96e 100644
--- a/front/src/Apps/Companies/CompanyAdvanced/RequestAdministrativeTransfer.tsx
+++ b/front/src/Apps/Companies/CompanyAdvanced/RequestAdministrativeTransfer.tsx
@@ -117,9 +117,9 @@ export function RequestAdministrativeTranfer({ company }: Props) {
return (
-
+
Transfert administratif de bordereaux
-
+
La fonctionnalité de transfert de bordereaux concerne les établissements
qui changent de SIRET mais conservent leurs activités. Elle permet de
diff --git a/front/src/Apps/Companies/CompanyAdvanced/advanced.scss b/front/src/Apps/Companies/CompanyAdvanced/advanced.scss
index fdacb0d33d..331797faeb 100644
--- a/front/src/Apps/Companies/CompanyAdvanced/advanced.scss
+++ b/front/src/Apps/Companies/CompanyAdvanced/advanced.scss
@@ -5,25 +5,30 @@
margin: 0;
padding: 0;
}
+
&__modal-title {
font-size: 1.5rem; //24px;
font-weight: 700;
margin: 0;
padding: 0;
}
+
&__description {
font-size: 0.875rem; //14px;
padding: 8px 0;
}
+
&__modal-description {
font-size: 0.875rem; //14px;
padding: 16px 0;
}
+
&__modal-cta {
display: flex;
justify-content: end;
gap: 10px;
}
+
&__section {
margin-bottom: 1.5rem;
}
diff --git a/front/src/Apps/Companies/CompanyDetails.tsx b/front/src/Apps/Companies/CompanyDetails.tsx
index 3679bcc5c8..da2ff5bdaa 100644
--- a/front/src/Apps/Companies/CompanyDetails.tsx
+++ b/front/src/Apps/Companies/CompanyDetails.tsx
@@ -22,12 +22,13 @@ import CompanyMembers from "./CompanyMembers/CompanyMembers";
import CompanyDigestSheetForm from "./CompanyDigestSheet/CompanyDigestSheet";
import { Tabs, TabsProps } from "@codegouvfr/react-dsfr/Tabs";
import { FrIconClassName } from "@codegouvfr/react-dsfr";
+import { CompanyRegistryDelegation } from "./CompanyRegistryDelegation/CompanyRegistryDelegation";
export type TabContentProps = {
company: CompanyPrivate;
};
-const COMPANY_DIGEST_FLAG = "COMPANY_DIGEST";
+const REGISTRY_V2_FLAG = "REGISTRY_V2";
const buildTabs = (
company: CompanyPrivate
@@ -36,10 +37,10 @@ const buildTabs = (
tabsContent: Record>;
} => {
const isAdmin = company.userRole === UserRole.Admin;
- const isMember = company.userRole === UserRole.Member;
- // Admin and member can access the gerico tab
- const canViewCompanyDigestTab =
- company.featureFlags.includes(COMPANY_DIGEST_FLAG) && (isAdmin || isMember);
+
+ // RNDTS features protected by feature flag
+ const canViewRndtsFeatures = company.featureFlags.includes(REGISTRY_V2_FLAG);
+
const iconId = "fr-icon-checkbox-line" as FrIconClassName;
const tabs = [
{
@@ -61,29 +62,35 @@ const buildTabs = (
tabId: "tab4",
label: "Contact",
iconId
+ },
+ {
+ tabId: "tab5",
+ label: "Fiche",
+ iconId
}
];
const tabsContent = {
tab1: CompanyInfo,
tab2: CompanySignature,
tab3: CompanyMembers,
- tab4: CompanyContactForm
+ tab4: CompanyContactForm,
+ tab5: CompanyDigestSheetForm
};
- if (canViewCompanyDigestTab) {
+ if (canViewRndtsFeatures) {
tabs.push({
- tabId: "tab5",
- label: "Fiche",
+ tabId: "tab6",
+ label: "Délégations RNDTS",
iconId
});
- tabsContent["tab5"] = CompanyDigestSheetForm;
+ tabsContent["tab6"] = CompanyRegistryDelegation;
}
if (isAdmin) {
tabs.push({
- tabId: "tab6",
+ tabId: "tab7",
label: "Avancé",
iconId
});
- tabsContent["tab6"] = CompanyAdvanced;
+ tabsContent["tab7"] = CompanyAdvanced;
}
return { tabs, tabsContent };
@@ -136,13 +143,15 @@ export default function CompanyDetails() {
>
}
>
-
-
-
+
+
+
+
+
);
}
diff --git a/front/src/Apps/Companies/CompanyMembers/CompanyMembersInvite.tsx b/front/src/Apps/Companies/CompanyMembers/CompanyMembersInvite.tsx
index 510cf95e36..7f5c0eaa0b 100644
--- a/front/src/Apps/Companies/CompanyMembers/CompanyMembersInvite.tsx
+++ b/front/src/Apps/Companies/CompanyMembers/CompanyMembersInvite.tsx
@@ -73,7 +73,7 @@ const CompanyMembersInvite = ({ company }: CompanyMembersInviteProps) => {
return (
<>
-
Inviter une personne
+
Inviter une personne