Bläddra i källkod

Adds AP2I request service and structure identifier

Introduces a new composable for interacting with the AP2I API,
including a service to handle requests and manage pending states.

Adds structure identifier field to the artist premium trial request form,
including validation for format and availability via the AP2I API.
The identifier is automatically generated from the structure name
unless manually modified by the user.

Adds environment variables for AP2I base URLs across different environments.
Olivier Massot 6 månader sedan
förälder
incheckning
3ed61397f5

+ 62 - 0
composables/data/useAp2iRequestService.ts

@@ -0,0 +1,62 @@
+import type { Ref } from 'vue'
+import type { FetchContext, FetchOptions } from 'ofetch'
+import ApiRequestService from '~/services/data/apiRequestService'
+
+/**
+ * Retourne une instance de ApiRequestService configurée pour interroger l'api Ap2i
+ *
+ * @see https://github.com/unjs/ohmyfetch/blob/main/README.md#%EF%B8%8F-create-fetch-with-default-options
+ */
+let apiRequestServiceClass: null | ApiRequestService = null
+
+export const useAp2iRequestService = () => {
+  const runtimeConfig = useRuntimeConfig()
+
+  const baseURL = runtimeConfig.ap2iBaseUrl ?? runtimeConfig.public.ap2iBaseUrl
+
+  const pending: Ref<boolean> = ref(false)
+
+  /**
+   * Peuple les headers avant l'envoi de la requête
+   *
+   * @param request
+   * @param options
+   */
+  const onRequest = function ({ request, options }: FetchContext) {
+    // @ts-ignore
+    if (options && options.noXaccessId) {
+      return
+    }
+
+    pending.value = true
+    console.log('Request : ' + request + ' (SSR: ' + process.server + ')')
+  }
+
+  const onResponse = function (_: FetchContext) {
+    pending.value = false
+  }
+
+  /**
+   * Gère les erreurs retournées par l'api
+   *
+   * @param request
+   * @param response
+   * @param error
+   */
+
+  const config: FetchOptions = {
+    baseURL,
+    onRequest,
+    onResponse,
+  }
+
+  // Avoid memory leak
+  if (apiRequestServiceClass === null) {
+    // Utilise la fonction `create` d'ohmyfetch pour générer un fetcher dédié à l'interrogation de Ap2i
+    const fetcher = $fetch.create(config)
+    // @ts-ignore
+    apiRequestServiceClass = new ApiRequestService(fetcher)
+  }
+
+  return { ap2iRequestService: apiRequestServiceClass, pending }
+}

+ 3 - 1
env/.env.ci

@@ -6,9 +6,11 @@ DEBUG=1
 NUXT_API_BASE_URL=none
 NUXT_PUBLIC_API_BASE_URL=none
 
+NUXT_AP2I_BASE_URL=none
+NUXT_PUBLIC_AP2I_BASE_URL=none
+
 NUXT_SITE_URL=none
 NUXT_PUBLIC_SITE_URL=none
 
 NUXT_AGENDA_BASE_URL=none
 NUXT_PUBLIC_AGENDA_BASE_URL=none
-

+ 3 - 0
env/.env.docker

@@ -6,6 +6,9 @@ DEBUG=1
 NUXT_API_BASE_URL=http://nginx_maestro
 NUXT_PUBLIC_API_BASE_URL=https://local.maestro.opentalent.fr
 
+NUXT_AP2I_BASE_URL=http://nginx_ap2i
+NUXT_PUBLIC_AP2I_BASE_URL=https://local.ap2i.opentalent.fr
+
 NUXT_SITE_URL=http://local.logiciels.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://local.logiciels.opentalent.fr
 

+ 3 - 0
env/.env.prod

@@ -8,6 +8,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.opentalent.fr
 

+ 3 - 0
env/.env.test

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test.opentalent.fr
 

+ 3 - 0
env/.env.test1

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test1.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test1.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test1.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test1.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test1.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test1.opentalent.fr
 

+ 3 - 0
env/.env.test2

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test2.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test2.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test2.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test2.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test2.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test2.opentalent.fr
 

+ 3 - 0
env/.env.test3

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test3.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test3.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test3.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test3.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test3.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test3.opentalent.fr
 

+ 3 - 0
env/.env.test4

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test4.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test4.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test4.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test4.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test4.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test4.opentalent.fr
 

+ 3 - 0
env/.env.test5

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test5.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test5.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test5.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test5.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test5.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test5.opentalent.fr
 

+ 3 - 0
env/.env.test6

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test6.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test6.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test6.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test6.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test6.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test6.opentalent.fr
 

+ 3 - 0
env/.env.test7

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test7.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test7.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test7.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test7.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test7.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test7.opentalent.fr
 

+ 3 - 0
env/.env.test8

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test8.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test8.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test8.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test8.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test8.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test8.opentalent.fr
 

+ 3 - 0
env/.env.test9

@@ -7,6 +7,9 @@ PORT=3003
 NUXT_API_BASE_URL=https://maestro.test9.opentalent.fr
 NUXT_PUBLIC_API_BASE_URL=https://maestro.test9.opentalent.fr
 
+NUXT_AP2I_BASE_URL=https://ap2i.test9.opentalent.fr
+NUXT_PUBLIC_AP2I_BASE_URL=https://ap2i.test9.opentalent.fr
+
 NUXT_SITE_URL=http://logiciels.test9.opentalent.fr
 NUXT_PUBLIC_SITE_URL=http://logiciels.test9.opentalent.fr
 

+ 2 - 0
nuxt.config.ts

@@ -41,6 +41,7 @@ export default defineNuxtConfig({
     env: '',
     siteUrl: '',
     apiBaseUrl: '',
+    ap2iBaseUrl: '',
     agendaBaseUrl: '',
     fileStorageBaseUrl: '',
     hCaptchaSiteKey: '35360874-ebb1-4748-86e3-9b156d5bfc53',
@@ -49,6 +50,7 @@ export default defineNuxtConfig({
       env: '',
       siteUrl: '',
       apiBaseUrl: '',
+      ap2iBaseUrl: '',
       agendaBaseUrl: '',
       fileStorageBaseUrl: '',
       hCaptchaSiteKey: '35360874-ebb1-4748-86e3-9b156d5bfc53',

+ 134 - 12
pages/shop/try/artist-premium.vue

@@ -85,6 +85,7 @@
                       :rules="[validateRequired]"
                       label="Nom de la structure*"
                       required
+                      @input="onStructureNameUpdated"
                     />
                   </v-col>
                 </v-row>
@@ -174,7 +175,43 @@
                   </v-col>
                 </v-row>
 
-
+                <!-- Structure identifier -->
+                <v-row>
+                  <v-col cols="12" md="6" class="mx-auto">
+                    <v-text-field
+                      v-model="trialRequest.structureIdentifier"
+                      :rules="[
+                        validateRequired,
+                        validateSubdomain,
+                        validateSubdomainAvailability,
+                      ]"
+                      label="Identifiant de la structure*"
+                      required
+                      class="text-center"
+                      @input="onStructureIdentifierUpdated"
+                    />
+                    <div class="validationMessage">
+                      <span v-if="validationPending">
+                        <v-progress-circular size="16" indeterminate />
+                        <i class="ml-2">Vérification en cours</i>
+                      </span>
+                      <span
+                        v-else-if="subdomainAvailable === true"
+                        class="text-success"
+                      >
+                        <v-icon>fa fa-check</v-icon>
+                        <i class="ml-2"> Cet identifiant est disponible</i>
+                      </span>
+                      <span
+                        v-else-if="subdomainAvailable === false"
+                        class="text-error"
+                      >
+                        <v-icon>fa fa-x</v-icon>
+                        <i class="ml-2">Cet identifiant n'est pas disponible</i>
+                      </span>
+                    </div>
+                  </v-col>
+                </v-row>
 
                 <h2 class="section-title">Représentée par</h2>
 
@@ -343,24 +380,34 @@
 import { useRouter } from 'vue-router'
 import type { Ref } from 'vue'
 import { reactive } from 'vue'
+import _ from 'lodash'
 import { useRuntimeConfig } from '#app'
 import type { TrialRequest } from '~/types/interface'
 import { STRUCTURE_TYPE, LEGAL_STATUS } from '~/types/types'
+import { useAp2iRequestService } from '~/composables/data/useAp2iRequestService'
+import { slugify } from '~/utils/string'
 
 const router = useRouter()
 
 const form: Ref<HTMLElement | null> = ref(null)
 
 // Structure types and legal statuses
-const structureTypes = Object.values(STRUCTURE_TYPE).map((item) => ({
-  value: item.key,
-  title: item.label,
-})).sort((a, b) => (a.title > b.title ? 1 : -1))
-
-const legalStatuses = Object.values(LEGAL_STATUS).map((item) => ({
-  value: item.key,
-  title: item.label,
-})).sort((a, b) => (a.title > b.title ? 1 : -1))
+const structureTypes = Object.values(STRUCTURE_TYPE)
+  .map((item) => ({
+    value: item.key,
+    title: item.label,
+  }))
+  .sort((a, b) => (a.title > b.title ? 1 : -1))
+
+const legalStatuses = Object.values(LEGAL_STATUS)
+  .map((item) => ({
+    value: item.key,
+    title: item.label,
+  }))
+  .sort((a, b) => (a.title > b.title ? 1 : -1))
+
+// Get apiRequestService for subdomain availability check
+const { ap2iRequestService } = useAp2iRequestService()
 
 // Trial request data
 const trialRequest = reactive<TrialRequest>({
@@ -372,6 +419,7 @@ const trialRequest = reactive<TrialRequest>({
   structureEmail: '',
   structureType: 'ARTISTIC_PRACTICE_ONLY',
   legalStatus: 'ASSOCIATION_LAW_1901',
+  structureIdentifier: '',
   siren: '',
   representativeFirstName: '',
   representativeLastName: '',
@@ -383,6 +431,49 @@ const trialRequest = reactive<TrialRequest>({
   newsletterSubscription: false,
 })
 
+// Track if structure identifier has been manually modified
+const structureIdentifierModified = ref(false)
+
+// Variables for subdomain validation
+const validationPending = ref(false)
+const subdomainAvailable = ref<boolean | null>(null)
+
+const checkSubdomainAvailability = async (subdomain: string) => {
+  if (!subdomain || validateSubdomain(subdomain) !== true) {
+    subdomainAvailable.value = null
+    return false
+  }
+
+  validationPending.value = true
+  try {
+    const subdomainAvailability = await ap2iRequestService.get(
+      '/api/public/subdomains/is_available',
+      { subdomain }
+    )
+
+    subdomainAvailable.value =
+      subdomainAvailability && subdomainAvailability.available === true
+    validationPending.value = false
+    return subdomainAvailable.value
+  } catch (error) {
+    console.error('Error checking subdomain availability:', error)
+    subdomainAvailable.value = false
+    validationPending.value = false
+    return false
+  }
+}
+
+/**
+ * Version debounced de la fonction checkAvailability
+ * @see https://docs-lodash.com/v4/debounce/
+ */
+const checkSubdomainAvailabilityDebounced: _.DebouncedFunc<() => void> =
+  _.debounce(
+    async () =>
+      await checkSubdomainAvailability(trialRequest.structureIdentifier),
+    600
+  )
+
 // Validation rules
 const validateRequired = (value: string) =>
   !!value || 'Ce champ est obligatoire'
@@ -403,6 +494,25 @@ const validateSiren = (siren: string) =>
 const validateCheckbox = (value: boolean) =>
   value || 'Vous devez accepter cette condition'
 
+const validateSubdomain = (value: string) => {
+  if (!value) return 'Ce champ est obligatoire'
+
+  const regex = /^[a-z0-9][a-z0-9-]{0,28}[a-z0-9]$/
+
+  return (
+    regex.test(value) ||
+    'Format invalide. Utilisez uniquement des lettres minuscules, des chiffres et des tirets. Doit commencer et finir par une lettre ou un chiffre. Maximum 30 caractères.'
+  )
+}
+
+const validateSubdomainAvailability = (value: string) => {
+  if (!value) return ''
+
+  return (
+    subdomainAvailable.value === true || "Cet identifiant n'est pas disponible"
+  )
+}
+
 // Form state
 const trialRequestSent: Ref<boolean> = ref(false)
 const errorMsg: Ref<string | null> = ref(null)
@@ -419,8 +529,6 @@ const submit = async (): Promise<void> => {
     const config = useRuntimeConfig()
     const apiUrl = `${config.public.apiBaseUrl}/trial/artists_premium`
 
-    console.log('Sending trial request to:', apiUrl)
-
     // Send the data to the API
     const response = await fetch(apiUrl, {
       method: 'POST',
@@ -448,6 +556,20 @@ const submit = async (): Promise<void> => {
       "Une erreur s'est produite lors de l'activation de votre essai. Veuillez réessayer plus tard ou nous contacter directement."
   }
 }
+
+// Event handler for structureName updates
+const onStructureNameUpdated = (newName: string) => {
+  if (!structureIdentifierModified.value && newName) {
+    trialRequest.structureIdentifier = slugify(trialRequest.structureName)
+    checkSubdomainAvailabilityDebounced()
+  }
+}
+
+// Event handler for structureIdentifier updates
+const onStructureIdentifierUpdated = () => {
+  structureIdentifierModified.value = true
+  checkSubdomainAvailabilityDebounced()
+}
 </script>
 
 <style scoped lang="scss">

+ 1 - 0
types/interface.d.ts

@@ -193,6 +193,7 @@ interface TrialRequest {
   structureEmail: string
   structureType: STRUCTURE_TYPE_KEYS
   legalStatus: LEGAL_STATUS_KEYS
+  structureIdentifier: string
   siren: string
   representativeFirstName: string
   representativeLastName: string

+ 25 - 0
utils/string.ts

@@ -0,0 +1,25 @@
+/**
+ * String utility functions
+ */
+
+/**
+ * Convert a string to a slug format
+ * Removes special characters, replaces spaces with hyphens, and converts to lowercase
+ *
+ * @param text The text to convert to slug format
+ * @returns The slugified text
+ */
+export function slugify(text: string): string {
+  return text
+    .toString()
+    .normalize('NFD') // Split accented characters
+    .replace(/[\u0300-\u036F]/g, '') // Remove diacritics
+    .toLowerCase()
+    .trim()
+    .replace(/\s+/g, '-') // Replace spaces with -
+    .replace(/[^\w-]+/g, '') // Remove all non-word chars
+    .replace(/--+/g, '-') // Replace multiple - with single -
+    .replace(/^-+/, '') // Trim - from start of text
+    .replace(/-+$/, '') // Trim - from end of text
+    .substring(0, 30) // Limit to 30 characters
+}