From 538a17031a83a42217f1020c23a1f9d2d84f569c Mon Sep 17 00:00:00 2001 From: Helena <37183360+helenarebane@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:06:24 +0300 Subject: [PATCH 1/2] B2B-52: add Connected Online syncing, tables and functions (#18) * B2B-52: add Connected Online syncing, tables and functions * clean up * improve autogenerated types * add use server directive --------- Co-authored-by: Helena --- .env.example | 2 + jobs/sync-analysis-groups.ts | 36 +- jobs/sync-connected-online.ts | 139 ++ lib/database.types.ts | 1442 ----------------- lib/services/audit.service.ts | 48 + lib/services/connected-online.service.ts | 268 +++ lib/services/medipost.service.ts | 2 + lib/types/audit.ts | 5 + lib/types/connected-online.ts | 226 +++ lib/types/external.ts | 4 + package.json | 27 +- pnpm-lock.yaml | 574 ++++--- supabase/database.types.ts | 296 ++++ ...0616142604_add_connected_online_tables.sql | 227 +++ 14 files changed, 1582 insertions(+), 1714 deletions(-) create mode 100644 jobs/sync-connected-online.ts delete mode 100644 lib/database.types.ts create mode 100644 lib/services/audit.service.ts create mode 100644 lib/services/connected-online.service.ts create mode 100644 lib/types/connected-online.ts create mode 100644 lib/types/external.ts create mode 100644 supabase/migrations/20250616142604_add_connected_online_tables.sql diff --git a/.env.example b/.env.example index 46d3948..2a3d662 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,8 @@ MEDIPOST_USER=your-medipost-user MEDIPOST_PASSWORD=your-medipost-password MEDIPOST_RECIPIENT=your-medipost-recipient +CONNECTED_ONLINE_URL=your-connected-online-url + EMAIL_SENDER= EMAIL_USER= # refer to your email provider's documentation EMAIL_PASSWORD= # refer to your email provider's documentation diff --git a/jobs/sync-analysis-groups.ts b/jobs/sync-analysis-groups.ts index e31c76b..ab34056 100644 --- a/jobs/sync-analysis-groups.ts +++ b/jobs/sync-analysis-groups.ts @@ -25,26 +25,32 @@ async function syncData() { config({ path: `.env.${process.env.NODE_ENV}` }); } - const baseUrl = process.env.MEDIPOST_URL!; - const user = process.env.MEDIPOST_USER!; - const password = process.env.MEDIPOST_PASSWORD!; - const sender = process.env.MEDIPOST_MESSAGE_SENDER!; + const baseUrl = process.env.MEDIPOST_URL; + const user = process.env.MEDIPOST_USER; + const password = process.env.MEDIPOST_PASSWORD; + const sender = process.env.MEDIPOST_MESSAGE_SENDER; + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseServiceRoleKey = + process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY; - if (!baseUrl || !user || !password || !sender) { + if ( + !baseUrl || + !supabaseUrl || + !supabaseServiceRoleKey || + !user || + !password || + !sender + ) { throw new Error('Could not access all necessary environment variables'); } - const supabase = createCustomClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!, - { - auth: { - persistSession: false, - autoRefreshToken: false, - detectSessionInUrl: false, - }, + const supabase = createCustomClient(supabaseUrl, supabaseServiceRoleKey, { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, }, - ); + }); try { // GET LATEST PUBLIC MESSAGE ID diff --git a/jobs/sync-connected-online.ts b/jobs/sync-connected-online.ts new file mode 100644 index 0000000..b655d86 --- /dev/null +++ b/jobs/sync-connected-online.ts @@ -0,0 +1,139 @@ +import { createClient as createCustomClient } from '@supabase/supabase-js'; + +import axios from 'axios'; +import { config } from 'dotenv'; + +async function syncData() { + if (process.env.NODE_ENV === 'local') { + config({ path: `.env.${process.env.NODE_ENV}` }); + } + + const baseUrl = process.env.CONNECTED_ONLINE_URL; + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseServiceRoleKey = + process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY; + + if (!baseUrl || !supabaseUrl || !supabaseServiceRoleKey) { + throw new Error('Could not access all necessary environment variables'); + } + + const supabase = createCustomClient(supabaseUrl, supabaseServiceRoleKey, { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, + }, + }); + + try { + const response = await axios.post( + `${baseUrl}/Search_Load`, + { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + param: "{'Value':'|et|-1'}", // get all available services in Estonian + }, + ); + + const responseData: { + Value: any; + Data: any; + ErrorCode: number; + ErrorMessage: string; + } = JSON.parse(response.data.d); + + if (responseData?.ErrorCode !== 0) { + throw new Error('Failed to get Connected Online data'); + } + + if ( + !responseData.Data.T_Lic?.length || + !responseData.Data.T_Service?.length + ) { + return supabase.schema('audit').from('sync_entries').insert({ + operation: 'CONNECTED_ONLINE_SYNC', + comment: 'No clinic or service data received', + status: 'FAIL', + changed_by_role: 'service_role', + }); + } + + const clinics = responseData.Data.T_Lic; + const services = responseData.Data.T_Service; + + const mappedClinics = clinics.map((clinic) => { + return { + id: clinic.ID, + can_select_worker: !!clinic.OnlineCanSelectWorker, + email: clinic.Email || null, + name: clinic.Name, + personal_code_required: !!clinic.PersonalCodeRequired, + phone_number: clinic.Phone || null, + }; + }); + + const mappedServices = services.map((service) => { + return { + id: service.ID, + clinic_id: service.ClinicID, + code: service.Code, + description: service.Description || null, + display: service.Display, + duration: service.Duration, + has_free_codes: !!service.HasFreeCodes, + name: service.Name, + neto_duration: service.NetoDuration, + online_hide_duration: service.OnlineHideDuration, + online_hide_price: service.OnlineHidePrice, + price: service.Price, + price_periods: service.PricePeriods || null, + requires_payment: !!service.RequiresPayment, + sync_id: service.SyncID, + }; + }); + + const { error: providersError } = await supabase + .from('connected_online_providers') + .upsert(mappedClinics); + + const { error: servicesError } = await supabase + .from('connected_online_services') + .upsert(mappedServices, { onConflict: 'id', ignoreDuplicates: false }); + + if (providersError || servicesError) { + return supabase + .schema('audit') + .from('sync_entries') + .insert({ + operation: 'CONNECTED_ONLINE_SYNC', + comment: providersError + ? 'Error saving providers: ' + JSON.stringify(providersError) + : 'Error saving services: ' + JSON.stringify(servicesError), + status: 'FAIL', + changed_by_role: 'service_role', + }); + } + + await supabase.schema('audit').from('sync_entries').insert({ + operation: 'CONNECTED_ONLINE_SYNC', + status: 'SUCCESS', + changed_by_role: 'service_role', + }); + } catch (e) { + await supabase + .schema('audit') + .from('sync_entries') + .insert({ + operation: 'CONNECTED_ONLINE_SYNC', + status: 'FAIL', + comment: JSON.stringify(e), + changed_by_role: 'service_role', + }); + throw new Error( + `Failed to sync Connected Online data, error: ${JSON.stringify(e)}`, + ); + } +} + +syncData(); diff --git a/lib/database.types.ts b/lib/database.types.ts deleted file mode 100644 index 452ae7b..0000000 --- a/lib/database.types.ts +++ /dev/null @@ -1,1442 +0,0 @@ -export type Json = - | string - | number - | boolean - | null - | { [key: string]: Json | undefined } - | Json[]; - -export type Database = { - graphql_public: { - Tables: { - [_ in never]: never; - }; - Views: { - [_ in never]: never; - }; - Functions: { - graphql: { - Args: { - operationName?: string; - query?: string; - variables?: Json; - extensions?: Json; - }; - Returns: Json; - }; - }; - Enums: { - [_ in never]: never; - }; - CompositeTypes: { - [_ in never]: never; - }; - }; - public: { - Tables: { - accounts: { - Row: { - created_at: string | null; - created_by: string | null; - email: string | null; - id: string; - is_personal_account: boolean; - name: string; - picture_url: string | null; - primary_owner_user_id: string; - public_data: Json; - slug: string | null; - updated_at: string | null; - updated_by: string | null; - }; - Insert: { - created_at?: string | null; - created_by?: string | null; - email?: string | null; - id?: string; - is_personal_account?: boolean; - name: string; - picture_url?: string | null; - primary_owner_user_id?: string; - public_data?: Json; - slug?: string | null; - updated_at?: string | null; - updated_by?: string | null; - }; - Update: { - created_at?: string | null; - created_by?: string | null; - email?: string | null; - id?: string; - is_personal_account?: boolean; - name?: string; - picture_url?: string | null; - primary_owner_user_id?: string; - public_data?: Json; - slug?: string | null; - updated_at?: string | null; - updated_by?: string | null; - }; - Relationships: []; - }; - accounts_memberships: { - Row: { - account_id: string; - account_role: string; - created_at: string; - created_by: string | null; - updated_at: string; - updated_by: string | null; - user_id: string; - }; - Insert: { - account_id: string; - account_role: string; - created_at?: string; - created_by?: string | null; - updated_at?: string; - updated_by?: string | null; - user_id: string; - }; - Update: { - account_id?: string; - account_role?: string; - created_at?: string; - created_by?: string | null; - updated_at?: string; - updated_by?: string | null; - user_id?: string; - }; - Relationships: [ - { - foreignKeyName: 'accounts_memberships_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'accounts_memberships_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'accounts_memberships_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'accounts_memberships_account_role_fkey'; - columns: ['account_role']; - isOneToOne: false; - referencedRelation: 'roles'; - referencedColumns: ['name']; - }, - ]; - }; - billing_customers: { - Row: { - account_id: string; - customer_id: string; - email: string | null; - id: number; - provider: Database['public']['Enums']['billing_provider']; - }; - Insert: { - account_id: string; - customer_id: string; - email?: string | null; - id?: number; - provider: Database['public']['Enums']['billing_provider']; - }; - Update: { - account_id?: string; - customer_id?: string; - email?: string | null; - id?: number; - provider?: Database['public']['Enums']['billing_provider']; - }; - Relationships: [ - { - foreignKeyName: 'billing_customers_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'billing_customers_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'billing_customers_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; - }, - ]; - }; - config: { - Row: { - billing_provider: Database['public']['Enums']['billing_provider']; - enable_account_billing: boolean; - enable_team_account_billing: boolean; - enable_team_accounts: boolean; - }; - Insert: { - billing_provider?: Database['public']['Enums']['billing_provider']; - enable_account_billing?: boolean; - enable_team_account_billing?: boolean; - enable_team_accounts?: boolean; - }; - Update: { - billing_provider?: Database['public']['Enums']['billing_provider']; - enable_account_billing?: boolean; - enable_team_account_billing?: boolean; - enable_team_accounts?: boolean; - }; - Relationships: []; - }; - invitations: { - Row: { - account_id: string; - created_at: string; - email: string; - expires_at: string; - id: number; - invite_token: string; - invited_by: string; - role: string; - updated_at: string; - }; - Insert: { - account_id: string; - created_at?: string; - email: string; - expires_at?: string; - id?: number; - invite_token: string; - invited_by: string; - role: string; - updated_at?: string; - }; - Update: { - account_id?: string; - created_at?: string; - email?: string; - expires_at?: string; - id?: number; - invite_token?: string; - invited_by?: string; - role?: string; - updated_at?: string; - }; - Relationships: [ - { - foreignKeyName: 'invitations_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'invitations_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'invitations_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'invitations_role_fkey'; - columns: ['role']; - isOneToOne: false; - referencedRelation: 'roles'; - referencedColumns: ['name']; - }, - ]; - }; - nonces: { - Row: { - client_token: string; - created_at: string; - description: string | null; - expires_at: string; - id: string; - last_verification_at: string | null; - last_verification_ip: unknown | null; - last_verification_user_agent: string | null; - metadata: Json | null; - nonce: string; - purpose: string; - revoked: boolean; - revoked_reason: string | null; - scopes: string[] | null; - tags: string[] | null; - used_at: string | null; - user_id: string | null; - verification_attempts: number; - }; - Insert: { - client_token: string; - created_at?: string; - description?: string | null; - expires_at: string; - id?: string; - last_verification_at?: string | null; - last_verification_ip?: unknown | null; - last_verification_user_agent?: string | null; - metadata?: Json | null; - nonce: string; - purpose: string; - revoked?: boolean; - revoked_reason?: string | null; - scopes?: string[] | null; - tags?: string[] | null; - used_at?: string | null; - user_id?: string | null; - verification_attempts?: number; - }; - Update: { - client_token?: string; - created_at?: string; - description?: string | null; - expires_at?: string; - id?: string; - last_verification_at?: string | null; - last_verification_ip?: unknown | null; - last_verification_user_agent?: string | null; - metadata?: Json | null; - nonce?: string; - purpose?: string; - revoked?: boolean; - revoked_reason?: string | null; - scopes?: string[] | null; - tags?: string[] | null; - used_at?: string | null; - user_id?: string | null; - verification_attempts?: number; - }; - Relationships: []; - }; - notifications: { - Row: { - account_id: string; - body: string; - channel: Database['public']['Enums']['notification_channel']; - created_at: string; - dismissed: boolean; - expires_at: string | null; - id: number; - link: string | null; - type: Database['public']['Enums']['notification_type']; - }; - Insert: { - account_id: string; - body: string; - channel?: Database['public']['Enums']['notification_channel']; - created_at?: string; - dismissed?: boolean; - expires_at?: string | null; - id?: never; - link?: string | null; - type?: Database['public']['Enums']['notification_type']; - }; - Update: { - account_id?: string; - body?: string; - channel?: Database['public']['Enums']['notification_channel']; - created_at?: string; - dismissed?: boolean; - expires_at?: string | null; - id?: never; - link?: string | null; - type?: Database['public']['Enums']['notification_type']; - }; - Relationships: [ - { - foreignKeyName: 'notifications_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'notifications_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'notifications_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; - }, - ]; - }; - order_items: { - Row: { - created_at: string; - id: string; - order_id: string; - price_amount: number | null; - product_id: string; - quantity: number; - updated_at: string; - variant_id: string; - }; - Insert: { - created_at?: string; - id: string; - order_id: string; - price_amount?: number | null; - product_id: string; - quantity?: number; - updated_at?: string; - variant_id: string; - }; - Update: { - created_at?: string; - id?: string; - order_id?: string; - price_amount?: number | null; - product_id?: string; - quantity?: number; - updated_at?: string; - variant_id?: string; - }; - Relationships: [ - { - foreignKeyName: 'order_items_order_id_fkey'; - columns: ['order_id']; - isOneToOne: false; - referencedRelation: 'orders'; - referencedColumns: ['id']; - }, - ]; - }; - orders: { - Row: { - account_id: string; - billing_customer_id: number; - billing_provider: Database['public']['Enums']['billing_provider']; - created_at: string; - currency: string; - id: string; - status: Database['public']['Enums']['payment_status']; - total_amount: number; - updated_at: string; - }; - Insert: { - account_id: string; - billing_customer_id: number; - billing_provider: Database['public']['Enums']['billing_provider']; - created_at?: string; - currency: string; - id: string; - status: Database['public']['Enums']['payment_status']; - total_amount: number; - updated_at?: string; - }; - Update: { - account_id?: string; - billing_customer_id?: number; - billing_provider?: Database['public']['Enums']['billing_provider']; - created_at?: string; - currency?: string; - id?: string; - status?: Database['public']['Enums']['payment_status']; - total_amount?: number; - updated_at?: string; - }; - Relationships: [ - { - foreignKeyName: 'orders_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'orders_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'orders_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'orders_billing_customer_id_fkey'; - columns: ['billing_customer_id']; - isOneToOne: false; - referencedRelation: 'billing_customers'; - referencedColumns: ['id']; - }, - ]; - }; - role_permissions: { - Row: { - id: number; - permission: Database['public']['Enums']['app_permissions']; - role: string; - }; - Insert: { - id?: number; - permission: Database['public']['Enums']['app_permissions']; - role: string; - }; - Update: { - id?: number; - permission?: Database['public']['Enums']['app_permissions']; - role?: string; - }; - Relationships: [ - { - foreignKeyName: 'role_permissions_role_fkey'; - columns: ['role']; - isOneToOne: false; - referencedRelation: 'roles'; - referencedColumns: ['name']; - }, - ]; - }; - roles: { - Row: { - hierarchy_level: number; - name: string; - }; - Insert: { - hierarchy_level: number; - name: string; - }; - Update: { - hierarchy_level?: number; - name?: string; - }; - Relationships: []; - }; - subscription_items: { - Row: { - created_at: string; - id: string; - interval: string; - interval_count: number; - price_amount: number | null; - product_id: string; - quantity: number; - subscription_id: string; - type: Database['public']['Enums']['subscription_item_type']; - updated_at: string; - variant_id: string; - }; - Insert: { - created_at?: string; - id: string; - interval: string; - interval_count: number; - price_amount?: number | null; - product_id: string; - quantity?: number; - subscription_id: string; - type: Database['public']['Enums']['subscription_item_type']; - updated_at?: string; - variant_id: string; - }; - Update: { - created_at?: string; - id?: string; - interval?: string; - interval_count?: number; - price_amount?: number | null; - product_id?: string; - quantity?: number; - subscription_id?: string; - type?: Database['public']['Enums']['subscription_item_type']; - updated_at?: string; - variant_id?: string; - }; - Relationships: [ - { - foreignKeyName: 'subscription_items_subscription_id_fkey'; - columns: ['subscription_id']; - isOneToOne: false; - referencedRelation: 'subscriptions'; - referencedColumns: ['id']; - }, - ]; - }; - subscriptions: { - Row: { - account_id: string; - active: boolean; - billing_customer_id: number; - billing_provider: Database['public']['Enums']['billing_provider']; - cancel_at_period_end: boolean; - created_at: string; - currency: string; - id: string; - period_ends_at: string; - period_starts_at: string; - status: Database['public']['Enums']['subscription_status']; - trial_ends_at: string | null; - trial_starts_at: string | null; - updated_at: string; - }; - Insert: { - account_id: string; - active: boolean; - billing_customer_id: number; - billing_provider: Database['public']['Enums']['billing_provider']; - cancel_at_period_end: boolean; - created_at?: string; - currency: string; - id: string; - period_ends_at: string; - period_starts_at: string; - status: Database['public']['Enums']['subscription_status']; - trial_ends_at?: string | null; - trial_starts_at?: string | null; - updated_at?: string; - }; - Update: { - account_id?: string; - active?: boolean; - billing_customer_id?: number; - billing_provider?: Database['public']['Enums']['billing_provider']; - cancel_at_period_end?: boolean; - created_at?: string; - currency?: string; - id?: string; - period_ends_at?: string; - period_starts_at?: string; - status?: Database['public']['Enums']['subscription_status']; - trial_ends_at?: string | null; - trial_starts_at?: string | null; - updated_at?: string; - }; - Relationships: [ - { - foreignKeyName: 'subscriptions_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'subscriptions_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_account_workspace'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'subscriptions_account_id_fkey'; - columns: ['account_id']; - isOneToOne: false; - referencedRelation: 'user_accounts'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'subscriptions_billing_customer_id_fkey'; - columns: ['billing_customer_id']; - isOneToOne: false; - referencedRelation: 'billing_customers'; - referencedColumns: ['id']; - }, - ]; - }; - }; - Views: { - user_account_workspace: { - Row: { - id: string | null; - name: string | null; - picture_url: string | null; - subscription_status: - | Database['public']['Enums']['subscription_status'] - | null; - }; - Relationships: []; - }; - user_accounts: { - Row: { - id: string | null; - name: string | null; - picture_url: string | null; - role: string | null; - slug: string | null; - }; - Relationships: [ - { - foreignKeyName: 'accounts_memberships_account_role_fkey'; - columns: ['role']; - isOneToOne: false; - referencedRelation: 'roles'; - referencedColumns: ['name']; - }, - ]; - }; - }; - Functions: { - accept_invitation: { - Args: { - token: string; - user_id: string; - }; - Returns: string; - }; - add_invitations_to_account: { - Args: { - account_slug: string; - invitations: Database['public']['CompositeTypes']['invitation'][]; - }; - Returns: Database['public']['Tables']['invitations']['Row'][]; - }; - can_action_account_member: { - Args: { - target_team_account_id: string; - target_user_id: string; - }; - Returns: boolean; - }; - create_invitation: { - Args: { - account_id: string; - email: string; - role: string; - }; - Returns: { - account_id: string; - created_at: string; - email: string; - expires_at: string; - id: number; - invite_token: string; - invited_by: string; - role: string; - updated_at: string; - }; - }; - create_nonce: { - Args: { - p_user_id?: string; - p_purpose?: string; - p_expires_in_seconds?: number; - p_metadata?: Json; - p_description?: string; - p_tags?: string[]; - p_scopes?: string[]; - p_revoke_previous?: boolean; - }; - Returns: Json; - }; - create_team_account: { - Args: { - account_name: string; - }; - Returns: { - created_at: string | null; - created_by: string | null; - email: string | null; - id: string; - is_personal_account: boolean; - name: string; - picture_url: string | null; - primary_owner_user_id: string; - public_data: Json; - slug: string | null; - updated_at: string | null; - updated_by: string | null; - }; - }; - get_account_invitations: { - Args: { - account_slug: string; - }; - Returns: { - id: number; - email: string; - account_id: string; - invited_by: string; - role: string; - created_at: string; - updated_at: string; - expires_at: string; - inviter_name: string; - inviter_email: string; - }[]; - }; - get_account_members: { - Args: { - account_slug: string; - }; - Returns: { - id: string; - user_id: string; - account_id: string; - role: string; - role_hierarchy_level: number; - primary_owner_user_id: string; - name: string; - email: string; - picture_url: string; - created_at: string; - updated_at: string; - }[]; - }; - get_config: { - Args: Record; - Returns: Json; - }; - get_nonce_status: { - Args: { - p_id: string; - }; - Returns: Json; - }; - get_upper_system_role: { - Args: Record; - Returns: string; - }; - has_active_subscription: { - Args: { - target_account_id: string; - }; - Returns: boolean; - }; - has_more_elevated_role: { - Args: { - target_user_id: string; - target_account_id: string; - role_name: string; - }; - Returns: boolean; - }; - has_permission: { - Args: { - user_id: string; - account_id: string; - permission_name: Database['public']['Enums']['app_permissions']; - }; - Returns: boolean; - }; - has_role_on_account: { - Args: { - account_id: string; - account_role?: string; - }; - Returns: boolean; - }; - has_same_role_hierarchy_level: { - Args: { - target_user_id: string; - target_account_id: string; - role_name: string; - }; - Returns: boolean; - }; - install_extensions: { - Args: Record; - Returns: undefined; - }; - is_aal2: { - Args: Record; - Returns: boolean; - }; - is_account_owner: { - Args: { - account_id: string; - }; - Returns: boolean; - }; - is_account_team_member: { - Args: { - target_account_id: string; - }; - Returns: boolean; - }; - is_mfa_compliant: { - Args: Record; - Returns: boolean; - }; - is_set: { - Args: { - field_name: string; - }; - Returns: boolean; - }; - is_super_admin: { - Args: Record; - Returns: boolean; - }; - is_team_member: { - Args: { - account_id: string; - user_id: string; - }; - Returns: boolean; - }; - revoke_nonce: { - Args: { - p_id: string; - p_reason?: string; - }; - Returns: boolean; - }; - team_account_workspace: { - Args: { - account_slug: string; - }; - Returns: { - id: string; - name: string; - picture_url: string; - slug: string; - role: string; - role_hierarchy_level: number; - primary_owner_user_id: string; - subscription_status: Database['public']['Enums']['subscription_status']; - permissions: Database['public']['Enums']['app_permissions'][]; - }[]; - }; - transfer_team_account_ownership: { - Args: { - target_account_id: string; - new_owner_id: string; - }; - Returns: undefined; - }; - upsert_order: { - Args: { - target_account_id: string; - target_customer_id: string; - target_order_id: string; - status: Database['public']['Enums']['payment_status']; - billing_provider: Database['public']['Enums']['billing_provider']; - total_amount: number; - currency: string; - line_items: Json; - }; - Returns: { - account_id: string; - billing_customer_id: number; - billing_provider: Database['public']['Enums']['billing_provider']; - created_at: string; - currency: string; - id: string; - status: Database['public']['Enums']['payment_status']; - total_amount: number; - updated_at: string; - }; - }; - upsert_subscription: { - Args: { - target_account_id: string; - target_customer_id: string; - target_subscription_id: string; - active: boolean; - status: Database['public']['Enums']['subscription_status']; - billing_provider: Database['public']['Enums']['billing_provider']; - cancel_at_period_end: boolean; - currency: string; - period_starts_at: string; - period_ends_at: string; - line_items: Json; - trial_starts_at?: string; - trial_ends_at?: string; - }; - Returns: { - account_id: string; - active: boolean; - billing_customer_id: number; - billing_provider: Database['public']['Enums']['billing_provider']; - cancel_at_period_end: boolean; - created_at: string; - currency: string; - id: string; - period_ends_at: string; - period_starts_at: string; - status: Database['public']['Enums']['subscription_status']; - trial_ends_at: string | null; - trial_starts_at: string | null; - updated_at: string; - }; - }; - verify_nonce: { - Args: { - p_token: string; - p_purpose: string; - p_user_id?: string; - p_required_scopes?: string[]; - p_max_verification_attempts?: number; - p_ip?: unknown; - p_user_agent?: string; - }; - Returns: Json; - }; - }; - Enums: { - app_permissions: - | 'roles.manage' - | 'billing.manage' - | 'settings.manage' - | 'members.manage' - | 'invites.manage'; - billing_provider: 'stripe' | 'lemon-squeezy' | 'paddle'; - notification_channel: 'in_app' | 'email'; - notification_type: 'info' | 'warning' | 'error'; - payment_status: 'pending' | 'succeeded' | 'failed'; - subscription_item_type: 'flat' | 'per_seat' | 'metered'; - subscription_status: - | 'active' - | 'trialing' - | 'past_due' - | 'canceled' - | 'unpaid' - | 'incomplete' - | 'incomplete_expired' - | 'paused'; - }; - CompositeTypes: { - invitation: { - email: string | null; - role: string | null; - }; - }; - }; - storage: { - Tables: { - buckets: { - Row: { - allowed_mime_types: string[] | null; - avif_autodetection: boolean | null; - created_at: string | null; - file_size_limit: number | null; - id: string; - name: string; - owner: string | null; - owner_id: string | null; - public: boolean | null; - updated_at: string | null; - }; - Insert: { - allowed_mime_types?: string[] | null; - avif_autodetection?: boolean | null; - created_at?: string | null; - file_size_limit?: number | null; - id: string; - name: string; - owner?: string | null; - owner_id?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - Update: { - allowed_mime_types?: string[] | null; - avif_autodetection?: boolean | null; - created_at?: string | null; - file_size_limit?: number | null; - id?: string; - name?: string; - owner?: string | null; - owner_id?: string | null; - public?: boolean | null; - updated_at?: string | null; - }; - Relationships: []; - }; - migrations: { - Row: { - executed_at: string | null; - hash: string; - id: number; - name: string; - }; - Insert: { - executed_at?: string | null; - hash: string; - id: number; - name: string; - }; - Update: { - executed_at?: string | null; - hash?: string; - id?: number; - name?: string; - }; - Relationships: []; - }; - objects: { - Row: { - bucket_id: string | null; - created_at: string | null; - id: string; - last_accessed_at: string | null; - metadata: Json | null; - name: string | null; - owner: string | null; - owner_id: string | null; - path_tokens: string[] | null; - updated_at: string | null; - user_metadata: Json | null; - version: string | null; - }; - Insert: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - owner_id?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - user_metadata?: Json | null; - version?: string | null; - }; - Update: { - bucket_id?: string | null; - created_at?: string | null; - id?: string; - last_accessed_at?: string | null; - metadata?: Json | null; - name?: string | null; - owner?: string | null; - owner_id?: string | null; - path_tokens?: string[] | null; - updated_at?: string | null; - user_metadata?: Json | null; - version?: string | null; - }; - Relationships: [ - { - foreignKeyName: 'objects_bucketId_fkey'; - columns: ['bucket_id']; - isOneToOne: false; - referencedRelation: 'buckets'; - referencedColumns: ['id']; - }, - ]; - }; - s3_multipart_uploads: { - Row: { - bucket_id: string; - created_at: string; - id: string; - in_progress_size: number; - key: string; - owner_id: string | null; - upload_signature: string; - user_metadata: Json | null; - version: string; - }; - Insert: { - bucket_id: string; - created_at?: string; - id: string; - in_progress_size?: number; - key: string; - owner_id?: string | null; - upload_signature: string; - user_metadata?: Json | null; - version: string; - }; - Update: { - bucket_id?: string; - created_at?: string; - id?: string; - in_progress_size?: number; - key?: string; - owner_id?: string | null; - upload_signature?: string; - user_metadata?: Json | null; - version?: string; - }; - Relationships: [ - { - foreignKeyName: 's3_multipart_uploads_bucket_id_fkey'; - columns: ['bucket_id']; - isOneToOne: false; - referencedRelation: 'buckets'; - referencedColumns: ['id']; - }, - ]; - }; - s3_multipart_uploads_parts: { - Row: { - bucket_id: string; - created_at: string; - etag: string; - id: string; - key: string; - owner_id: string | null; - part_number: number; - size: number; - upload_id: string; - version: string; - }; - Insert: { - bucket_id: string; - created_at?: string; - etag: string; - id?: string; - key: string; - owner_id?: string | null; - part_number: number; - size?: number; - upload_id: string; - version: string; - }; - Update: { - bucket_id?: string; - created_at?: string; - etag?: string; - id?: string; - key?: string; - owner_id?: string | null; - part_number?: number; - size?: number; - upload_id?: string; - version?: string; - }; - Relationships: [ - { - foreignKeyName: 's3_multipart_uploads_parts_bucket_id_fkey'; - columns: ['bucket_id']; - isOneToOne: false; - referencedRelation: 'buckets'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 's3_multipart_uploads_parts_upload_id_fkey'; - columns: ['upload_id']; - isOneToOne: false; - referencedRelation: 's3_multipart_uploads'; - referencedColumns: ['id']; - }, - ]; - }; - }; - Views: { - [_ in never]: never; - }; - Functions: { - can_insert_object: { - Args: { - bucketid: string; - name: string; - owner: string; - metadata: Json; - }; - Returns: undefined; - }; - extension: { - Args: { - name: string; - }; - Returns: string; - }; - filename: { - Args: { - name: string; - }; - Returns: string; - }; - foldername: { - Args: { - name: string; - }; - Returns: string[]; - }; - get_size_by_bucket: { - Args: Record; - Returns: { - size: number; - bucket_id: string; - }[]; - }; - list_multipart_uploads_with_delimiter: { - Args: { - bucket_id: string; - prefix_param: string; - delimiter_param: string; - max_keys?: number; - next_key_token?: string; - next_upload_token?: string; - }; - Returns: { - key: string; - id: string; - created_at: string; - }[]; - }; - list_objects_with_delimiter: { - Args: { - bucket_id: string; - prefix_param: string; - delimiter_param: string; - max_keys?: number; - start_after?: string; - next_token?: string; - }; - Returns: { - name: string; - id: string; - metadata: Json; - updated_at: string; - }[]; - }; - operation: { - Args: Record; - Returns: string; - }; - search: { - Args: { - prefix: string; - bucketname: string; - limits?: number; - levels?: number; - offsets?: number; - search?: string; - sortcolumn?: string; - sortorder?: string; - }; - Returns: { - name: string; - id: string; - updated_at: string; - created_at: string; - last_accessed_at: string; - metadata: Json; - }[]; - }; - }; - Enums: { - [_ in never]: never; - }; - CompositeTypes: { - [_ in never]: never; - }; - }; -}; - -type PublicSchema = Database[Extract]; - -export type Tables< - PublicTableNameOrOptions extends - | keyof (PublicSchema['Tables'] & PublicSchema['Views']) - | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] & - Database[PublicTableNameOrOptions['schema']]['Views']) - : never = never, -> = PublicTableNameOrOptions extends { schema: keyof Database } - ? (Database[PublicTableNameOrOptions['schema']]['Tables'] & - Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends { - Row: infer R; - } - ? R - : never - : PublicTableNameOrOptions extends keyof (PublicSchema['Tables'] & - PublicSchema['Views']) - ? (PublicSchema['Tables'] & - PublicSchema['Views'])[PublicTableNameOrOptions] extends { - Row: infer R; - } - ? R - : never - : never; - -export type TablesInsert< - PublicTableNameOrOptions extends - | keyof PublicSchema['Tables'] - | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] - : never = never, -> = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { - Insert: infer I; - } - ? I - : never - : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] - ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { - Insert: infer I; - } - ? I - : never - : never; - -export type TablesUpdate< - PublicTableNameOrOptions extends - | keyof PublicSchema['Tables'] - | { schema: keyof Database }, - TableName extends PublicTableNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicTableNameOrOptions['schema']]['Tables'] - : never = never, -> = PublicTableNameOrOptions extends { schema: keyof Database } - ? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends { - Update: infer U; - } - ? U - : never - : PublicTableNameOrOptions extends keyof PublicSchema['Tables'] - ? PublicSchema['Tables'][PublicTableNameOrOptions] extends { - Update: infer U; - } - ? U - : never - : never; - -export type Enums< - PublicEnumNameOrOptions extends - | keyof PublicSchema['Enums'] - | { schema: keyof Database }, - EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database } - ? keyof Database[PublicEnumNameOrOptions['schema']]['Enums'] - : never = never, -> = PublicEnumNameOrOptions extends { schema: keyof Database } - ? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName] - : PublicEnumNameOrOptions extends keyof PublicSchema['Enums'] - ? PublicSchema['Enums'][PublicEnumNameOrOptions] - : never; - -export type CompositeTypes< - PublicCompositeTypeNameOrOptions extends - | keyof PublicSchema['CompositeTypes'] - | { schema: keyof Database }, - CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { - schema: keyof Database; - } - ? keyof Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'] - : never = never, -> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } - ? Database[PublicCompositeTypeNameOrOptions['schema']]['CompositeTypes'][CompositeTypeName] - : PublicCompositeTypeNameOrOptions extends keyof PublicSchema['CompositeTypes'] - ? PublicSchema['CompositeTypes'][PublicCompositeTypeNameOrOptions] - : never; diff --git a/lib/services/audit.service.ts b/lib/services/audit.service.ts new file mode 100644 index 0000000..994a25b --- /dev/null +++ b/lib/services/audit.service.ts @@ -0,0 +1,48 @@ + 'use server' + +import { createClient } from '@supabase/supabase-js'; + +import { RequestStatus } from '@/lib/types/audit'; +import { ConnectedOnlineMethodName } from '@/lib/types/connected-online'; +import { ExternalApi } from '@/lib/types/external'; +import { MedipostAction } from '@/lib/types/medipost'; + +export default async function logRequestResult( + /* personalCode: string, */ requestApi: keyof typeof ExternalApi, + requestApiMethod: `${ConnectedOnlineMethodName}` | `${MedipostAction}`, + status: RequestStatus, + comment?: string, + startTime?: string, + serviceId?: number, + serviceProviderId?: number, +) { + const supabaseServiceUser = createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!, + { + auth: { + persistSession: false, + autoRefreshToken: false, + detectSessionInUrl: false, + }, + }, + ); + + const { error } = await supabaseServiceUser + .schema('audit') + .from('request_entries') + .insert({ + /* personal_code: personalCode, */ + request_api: requestApi, + request_api_method: requestApiMethod, + requested_start_date: startTime, + status, + service_id: serviceId, + service_provider_id: serviceProviderId, + comment, + }); + + if (error) { + throw new Error('Failed to insert log entry, error: ' + error.message); + } +} diff --git a/lib/services/connected-online.service.ts b/lib/services/connected-online.service.ts new file mode 100644 index 0000000..8a42a21 --- /dev/null +++ b/lib/services/connected-online.service.ts @@ -0,0 +1,268 @@ + 'use server' + +import logRequestResult from '@/lib/services/audit.service'; +import { RequestStatus } from '@/lib/types/audit'; +import { + AvailableAppointmentsResponse, + BookTimeResponse, + ConfirmedLoadResponse, + ConnectedOnlineMethodName, +} from '@/lib/types/connected-online'; +import { ExternalApi } from '@/lib/types/external'; +import { Tables } from '@/supabase/database.types'; +import { createClient } from '@/utils/supabase/server'; +import axios from 'axios'; + +export async function getAvailableAppointmentsForService( + serviceId: number, + startTime?: Date, +) { + try { + const showTimesFrom = startTime ? { StartTime: startTime } : {}; + + const response = await axios.post( + `${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.GetAvailabilities}`, + { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + param: JSON.stringify({ + ServiceID: serviceId, + Key: '7T624nlu', + Lang: 'et', + ...showTimesFrom, + }), + }, + ); + + const responseData: AvailableAppointmentsResponse = JSON.parse( + response.data.d, + ); + + if ( + responseData?.ErrorCode !== 0 || + !responseData.Data.T_Service?.length || + !responseData.Data.T_Booking?.length + ) { + let comment = `Response returned error code ${responseData.ErrorCode}, message: ${responseData.ErrorMessage}`; + if (responseData?.ErrorCode === 0) { + comment = responseData.Data.T_Service?.length + ? `No service present in appointment availability response, service id: ${serviceId}, start time: ${startTime}` + : `No booking times present in appointment availability response, service id: ${serviceId}, start time: ${startTime}`; + } + + await logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.GetAvailabilities, + RequestStatus.Fail, + comment, + ); + + return null; + } + + await logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.GetAvailabilities, + RequestStatus.Success, + JSON.stringify(responseData), + ); + return responseData.Data; + } catch (error) { + await logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.GetAvailabilities, + RequestStatus.Fail, + JSON.stringify(error), + ); + return null; + } +} + +export async function bookAppointment( + serviceSyncId: number, + clinicId: number, + appointmentUserId: number, + syncUserID: number, + startTime: string, + locationId = 0, + comments = '', + isEarlierTimeRequested = false, + earlierTimeRequestComment = '', +) { + const supabase = await createClient(); + + try { + const { + data: { user }, + } = await supabase.auth.getUser(); + + if (!user?.id) { + throw new Error('User not authenticated'); + } + + const [ + { data: dbClinic, error: clinicError }, + { data: dbService, error: serviceError }, + ] = await Promise.all([ + supabase + .from('connected_online_providers') + .select('*') + .eq('id', clinicId) + .limit(1), + supabase + .from('connected_online_services') + .select('*') + .eq('sync_id', serviceSyncId) + .eq('clinic_id', clinicId) + .limit(1), + ]); + + if (!dbClinic?.length || !dbService?.length) { + return logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.BookTime, + RequestStatus.Fail, + dbClinic?.length + ? `Could not find clinic with id ${clinicId}, error: ${JSON.stringify(clinicError)}` + : `Could not find service with sync id ${serviceSyncId} and clinic id ${clinicId}, error: ${JSON.stringify(serviceError)}`, + startTime, + serviceSyncId, + clinicId, + ); + } + + const clinic: Tables<'connected_online_providers'> = dbClinic![0]; + const service: Tables<'connected_online_services'> = dbService![0]; + + // TODO the dummy data needs to be replaced with real values once they're present on the user/account + const response = await axios.post( + `${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.BookTime}`, + { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + param: JSON.stringify({ + EarlierTime: isEarlierTimeRequested, // once we have the e-shop functionality we can let the user select if she would like to be offered earlier time slots if they become available + EarlierTimeComment: earlierTimeRequestComment, + ClinicID: clinic.id, + ServiceID: service.id, + ClinicServiceID: service.sync_id, + UserID: appointmentUserId, + SyncUserID: syncUserID, + StartTime: startTime, + FirstName: 'Test', + LastName: 'User', + PersonalCode: '4', + Email: user.email, + Phone: 'phone', + Comments: comments, + Location: locationId, + FreeCode: '', + AddToBasket: false, + Key: '7T624nlu', + Lang: 'et', // update when integrated into app, if needed + }), + }, + ); + + const responseData: BookTimeResponse = JSON.parse(response.data.d); + + if (responseData?.ErrorCode !== 0 || !responseData.Value) { + return logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.BookTime, + RequestStatus.Fail, + JSON.stringify(responseData), + startTime, + service.id, + clinicId, + ); + } + + const responseParts = responseData.Value.split(','); + + const { error } = await supabase + .from('connected_online_reservation') + .insert({ + booking_code: responseParts[1], + clinic_id: clinic.id, + comments, + lang: 'et', // change later, if needed + service_id: service.id, + service_user_id: appointmentUserId, + start_time: startTime, + sync_user_id: syncUserID, + requires_payment: !!responseParts[0], + user_id: user.id, + }); + + await logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.BookTime, + RequestStatus.Success, + JSON.stringify(responseData), + startTime, + service.id, + clinicId, + ); + + if (error) { + throw new Error(error.message); + } + + return responseData.Value; + } catch (error) { + return logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.BookTime, + RequestStatus.Fail, + JSON.stringify(error), + startTime, + serviceSyncId, + clinicId, + ); + } +} + +export async function getConfirmedService(reservationCode: string) { + try { + const response = await axios.post( + `${process.env.CONNECTED_ONLINE_URL!}/${ConnectedOnlineMethodName.ConfirmedLoad}`, + { + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + param: JSON.stringify({ Value: `${reservationCode}|7T624nlu|et` }), + }, + ); + + const responseData: ConfirmedLoadResponse = JSON.parse(response.data.d); + + if (responseData?.ErrorCode !== 0) { + await logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.ConfirmedLoad, + RequestStatus.Fail, + JSON.stringify(responseData), + ); + return null; + } + + await logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.ConfirmedLoad, + RequestStatus.Success, + JSON.stringify(responseData), + ); + return responseData.Data; + } catch (error) { + await logRequestResult( + ExternalApi.ConnectedOnline, + ConnectedOnlineMethodName.ConfirmedLoad, + RequestStatus.Fail, + JSON.stringify(error), + ); + return null; + } +} diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts index 994581b..355d7d7 100644 --- a/lib/services/medipost.service.ts +++ b/lib/services/medipost.service.ts @@ -1,3 +1,5 @@ +'use server'; + import { SupabaseClient, createClient as createCustomClient, diff --git a/lib/types/audit.ts b/lib/types/audit.ts index f8eed24..a4985ab 100644 --- a/lib/types/audit.ts +++ b/lib/types/audit.ts @@ -2,3 +2,8 @@ export enum SyncStatus { Success = "SUCCESS", Fail = "FAIL", } + +export enum RequestStatus { + Success = "SUCCESS", + Fail = "FAIL", +} diff --git a/lib/types/connected-online.ts b/lib/types/connected-online.ts new file mode 100644 index 0000000..db6205d --- /dev/null +++ b/lib/types/connected-online.ts @@ -0,0 +1,226 @@ +import * as z from 'zod'; + +export const BookTimeResponseSchema = z.object({ + Value: z.string(), + Data: z.null(), + ErrorCode: z.number(), + ErrorMessage: z.null(), +}); +export type BookTimeResponse = z.infer; + +export enum ConnectedOnlineMethodName { + SearchLoad = 'Search_Load', + GetAvailabilities = 'GetAvailabilities', + BookTime = 'BookTime', + ConfirmedLoad = 'Confirmed_Load', +} + +export const AvailableAppointmentTBookingSchema = z.object({ + ClinicID: z.string(), + LocationID: z.number(), + UserID: z.number(), + SyncUserID: z.number(), + ServiceID: z.number(), + HKServiceID: z.number(), + StartTime: z.coerce.date(), + EndTime: z.coerce.date(), + PayorCode: z.string(), +}); +export type AvailableAppointment = z.infer< + typeof AvailableAppointmentTBookingSchema +>; + +export const TDocsASchema = z.object({ + List: z.string(), +}); +export type TDocsA = z.infer; + +export const TServiceSchema = z.object({ + Code: z.string(), + Name: z.string(), + Price: z.number(), + VATType: z.number(), + HKServiceID: z.union([z.number(), z.null()]), + Duration: z.union([z.number(), z.null()]), + Description: z.union([z.string(), z.null()]), + OnlinePaymentRequired: z.number(), + TehikServiceCode: z.union([z.string(), z.null()]), + OnlineHideDuration: z.number(), + OnlineHidePrice: z.number(), + PricePeriods: z.string(), +}); +export type TService = z.infer; + +export const AvailableAppointmentsDataSchema = z.object({ + T_ScheduleType: z.array(z.any()), + T_Service: z.array(TServiceSchema), + T_Booking: z.array(AvailableAppointmentTBookingSchema), + T_DocsAvailable: z.array(TDocsASchema), + T_DocsAll: z.array(TDocsASchema), +}); +export type AvailableAppointmentsData = z.infer< + typeof AvailableAppointmentsDataSchema +>; + +export const AvailableAppointmentsResponseSchema = z.object({ + Value: z.null(), + Data: AvailableAppointmentsDataSchema, + ErrorCode: z.number(), + ErrorMessage: z.union([z.string(), z.null()]), +}); +export type AvailableAppointmentsResponse = z.infer< + typeof AvailableAppointmentsResponseSchema +>; + +export const APaymentRequestSchema = z.object({ + ID: z.number(), + Ref: z.string(), + AmountTotal: z.number(), + AmountVat: z.number(), + PaidAmount: z.union([z.number(), z.null()]), + Paid: z.union([z.number(), z.null()]), + Failed: z.union([z.number(), z.null()]), + PaidAmount1: z.union([z.number(), z.null()]), + ScheduleID: z.number(), + PaymentID: z.union([z.number(), z.null()]), + Created: z.coerce.date(), + Nonce: z.string(), + DiscountPercent: z.number(), + DiscountCode: z.string(), +}); +export type APaymentRequest = z.infer; + +export const PClinicSchema = z.object({ + ID: z.number(), + LicenseID: z.number(), + Name: z.string(), + Email: z.string(), + Address: z.string(), + Address2: z.string(), + Phone: z.string(), + VatNo: z.string(), + RegistryCode: z.string(), + OID: z.string(), + PersonalCodeRequired: z.number(), + OnlineCommentRequired: z.number(), + OnlineLoginRequired: z.number(), + OnlineSenderEmail: z.string(), + OnlineReplyToEmail: z.string(), + OnlineCCEmail: z.string(), + OnlineRedirectkUrl: z.string(), // the typo is on their side + OnlineAllowQueue: z.number(), +}); +export type PClinic = z.infer; + +export const PJobTitleTranslationSchema = z.object({ + ID: z.number(), + SyncID: z.number(), + TextEN: z.string(), + TextET: z.string(), + TextFI: z.string(), + TextRU: z.string(), + TextLT: z.string(), + ClinicID: z.number(), + Deleted: z.number(), +}); +export type PJobTitleTranslation = z.infer; + +export const PServiceSchema = z.object({ + ID: z.number(), + NameET: z.string(), + NameEN: z.string(), + NameRU: z.string(), + NameFI: z.string(), + DescriptionET: z.string(), + DescriptionEN: z.string(), + DescriptionRU: z.string(), + DescriptionFI: z.string(), + ExtraEmailTextET: z.string(), + ExtraEmailTextEN: z.string(), + ExtraEmailTextRU: z.string(), + ExtraEmailTextFI: z.string(), +}); +export type PService = z.infer; + +export const ParamSchema = z.object({ + PersonalCodeRequired: z.number(), + OnlineCommentRequired: z.number(), + OnlineLoginRequired: z.number(), + ClinicName: z.string(), + ClinicID: z.number(), + OnlineRedirectkUrl: z.string(), + OnlineAllowQueue: z.number(), + Key: z.string(), +}); +export type Param = z.infer; + +export const TBookingSchema = z.object({ + ID: z.number(), + ClinicID: z.number(), + FirstName: z.string(), + LastName: z.string(), + Email: z.string(), + Phone: z.string(), + PersonalCode: z.string(), + Comments: z.string(), + ServiceName: z.string(), + DoctorName: z.string(), + StartTime: z.coerce.date(), + EndTime: z.coerce.date(), + Status: z.number(), + PaymentRequestID: z.number(), + MailResponse: z.null(), // was not present in test data, might need to be specified in the future + BookingCode: z.string(), + LocationName: z.union([z.string(), z.null()]), + isDR: z.number(), + LocationID: z.number(), + ScheduleID: z.number(), + ServiceID: z.number(), + ServiceSyncID: z.number(), + ServiceCode: z.string(), + ServiceIsRemote: z.number(), + IsRegistered: z.number(), + OfferEarlierTime: z.number(), + EarlierTimeComment: z.string(), + PartnerCode: z.union([z.number(), z.null()]), + PartnerCallBackUrl: z.union([z.string(), z.null()]), + LocationOfficialName: z.union([z.string(), z.null()]), + LocationAddress1: z.union([z.string(), z.null()]), + LocationAddress2: z.union([z.string(), z.null()]), + LocationPhone: z.union([z.string(), z.null()]), +}); +export type TBooking = z.infer; + +export const TDoctorSchema = z.object({ + ID: z.number(), + Name: z.string(), + Prefix: z.string(), + Photo: z.string(), + SpokenLanguages: z.string(), + JobTitleID: z.union([z.number(), z.null()]), + ClinicID: z.number(), + Deleted: z.number(), +}); +export type TDoctor = z.infer; + +export const ConfirmedLoadDataSchema = z.object({ + T_Booking: z.array(TBookingSchema), + P_Location: z.array(z.any()), + P_Clinic: z.array(PClinicSchema), + A_PaymentRequest: z.array(APaymentRequestSchema), + P_Service: z.array(PServiceSchema), + T_Service: z.array(z.any()), + T_ServiceHK: z.array(z.any()), + P_JobTitleTranslations: z.array(PJobTitleTranslationSchema), + T_Doctor: z.array(TDoctorSchema), + Params: z.array(ParamSchema), +}); +export type ConfirmedLoadData = z.infer; + +export const ConfirmedLoadResponseSchema = z.object({ + Value: z.null(), + Data: ConfirmedLoadDataSchema, + ErrorCode: z.number(), + ErrorMessage: z.union([z.string(), z.null()]), +}); +export type ConfirmedLoadResponse = z.infer; diff --git a/lib/types/external.ts b/lib/types/external.ts new file mode 100644 index 0000000..df93a42 --- /dev/null +++ b/lib/types/external.ts @@ -0,0 +1,4 @@ +export enum ExternalApi { + Medipost = 'Medipost', + ConnectedOnline = 'ConnectedOnline', +} diff --git a/package.json b/package.json index be13199..b85c538 100644 --- a/package.json +++ b/package.json @@ -19,21 +19,22 @@ "supabase": "supabase", "supabase:start": "supabase status || supabase start", "supabase:stop": "supabase stop", - "supabase:reset": "supabase db reset", "supabase:status": "supabase status", "supabase:test": "supabase db test", + "supabase:db:reset": "supabase db reset", "supabase:db:lint": "supabase db lint", "supabase:db:diff": "supabase db diff", "supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push", "supabase:typegen": "pnpm run supabase:typegen:packages && pnpm run supabase:typegen:app", "supabase:typegen:packages": "supabase gen types typescript --local > ../../packages/supabase/src/database.types.ts", - "supabase:typegen:app": "supabase gen types typescript --local > ./lib/database.types.ts", + "supabase:typegen:app": "supabase gen types typescript --local > ./supabase/database.types.ts", "supabase:db:dump:local": "supabase db dump --local --data-only", - "sync-data:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts" + "sync-analysis-groups:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts", + "sync-connected-online:dev": "NODE_ENV=local ts-node jobs/sync-connected-online.ts" }, "dependencies": { "@edge-csrf/nextjs": "2.5.3-cloudflare-rc1", - "@hookform/resolvers": "^5.0.1", + "@hookform/resolvers": "^5.1.1", "@kit/accounts": "workspace:*", "@kit/admin": "workspace:*", "@kit/analytics": "workspace:*", @@ -61,7 +62,7 @@ "@supabase/supabase-js": "2.49.4", "@tanstack/react-query": "5.76.1", "@tanstack/react-table": "^8.21.3", - "axios": "^1.9.0", + "axios": "^1.10.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", "fast-xml-parser": "^5.2.5", @@ -72,13 +73,13 @@ "next-themes": "0.4.6", "react": "19.1.0", "react-dom": "19.1.0", - "react-hook-form": "^7.56.3", - "react-i18next": "^15.5.1", + "react-hook-form": "^7.58.0", + "react-i18next": "^15.5.3", "recharts": "2.15.3", - "sonner": "^2.0.3", - "tailwind-merge": "^3.3.0", + "sonner": "^2.0.5", + "tailwind-merge": "^3.3.1", "ts-node": "^10.9.2", - "zod": "^3.24.4" + "zod": "^3.25.67" }, "devDependencies": { "@hookform/resolvers": "^5.0.1", @@ -86,9 +87,9 @@ "@kit/prettier-config": "workspace:*", "@kit/tsconfig": "workspace:*", "@next/bundle-analyzer": "15.3.2", - "@tailwindcss/postcss": "^4.1.7", + "@tailwindcss/postcss": "^4.1.10", "@types/lodash": "^4.17.17", - "@types/node": "^22.15.18", + "@types/node": "^22.15.32", "@types/react": "19.1.4", "@types/react-dom": "19.1.5", "babel-plugin-react-compiler": "19.1.0-rc.2", @@ -97,7 +98,7 @@ "pino-pretty": "^13.0.0", "prettier": "^3.5.3", "react-hook-form": "^7.57.0", - "supabase": "^2.22.12", + "supabase": "^2.26.9", "tailwindcss": "4.1.7", "tailwindcss-animate": "^1.0.7", "typescript": "^5.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c05d79..1837337 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: 2.5.3-cloudflare-rc1 version: 2.5.3-cloudflare-rc1(next@15.3.2(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) '@hookform/resolvers': - specifier: ^5.0.1 - version: 5.0.1(react-hook-form@7.57.0(react@19.1.0)) + specifier: ^5.1.1 + version: 5.1.1(react-hook-form@7.58.0(react@19.1.0)) '@kit/accounts': specifier: workspace:* version: link:packages/features/accounts @@ -96,8 +96,8 @@ importers: specifier: ^8.21.3 version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) axios: - specifier: ^1.9.0 - version: 1.9.0 + specifier: ^1.10.0 + version: 1.10.0 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -129,26 +129,26 @@ importers: specifier: 19.1.0 version: 19.1.0(react@19.1.0) react-hook-form: - specifier: ^7.56.3 - version: 7.57.0(react@19.1.0) + specifier: ^7.58.0 + version: 7.58.0(react@19.1.0) react-i18next: - specifier: ^15.5.1 - version: 15.5.2(i18next@25.1.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) + specifier: ^15.5.3 + version: 15.5.3(i18next@25.1.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) recharts: specifier: 2.15.3 version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) sonner: - specifier: ^2.0.3 + specifier: ^2.0.5 version: 2.0.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) tailwind-merge: - specifier: ^3.3.0 - version: 3.3.0 + specifier: ^3.3.1 + version: 3.3.1 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@22.15.30)(typescript@5.8.3) + version: 10.9.2(@types/node@22.15.32)(typescript@5.8.3) zod: - specifier: ^3.24.4 - version: 3.25.56 + specifier: ^3.25.67 + version: 3.25.67 devDependencies: '@kit/eslint-config': specifier: workspace:* @@ -163,14 +163,14 @@ importers: specifier: 15.3.2 version: 15.3.2 '@tailwindcss/postcss': - specifier: ^4.1.7 - version: 4.1.8 + specifier: ^4.1.10 + version: 4.1.10 '@types/lodash': specifier: ^4.17.17 version: 4.17.17 '@types/node': - specifier: ^22.15.18 - version: 22.15.30 + specifier: ^22.15.32 + version: 22.15.32 '@types/react': specifier: 19.1.4 version: 19.1.4 @@ -182,7 +182,7 @@ importers: version: 19.1.0-rc.2 cssnano: specifier: ^7.0.7 - version: 7.0.7(postcss@8.5.4) + version: 7.0.7(postcss@8.5.6) dotenv: specifier: ^16.5.0 version: 16.5.0 @@ -193,8 +193,8 @@ importers: specifier: ^3.5.3 version: 3.5.3 supabase: - specifier: ^2.22.12 - version: 2.24.3 + specifier: ^2.26.9 + version: 2.26.9 tailwindcss: specifier: 4.1.7 version: 4.1.7 @@ -354,7 +354,7 @@ importers: version: 7.3.1 stripe: specifier: ^18.1.0 - version: 18.2.1(@types/node@22.15.30) + version: 18.2.1(@types/node@24.0.3) devDependencies: '@kit/billing': specifier: workspace:* @@ -1736,6 +1736,11 @@ packages: peerDependencies: react-hook-form: ^7.55.0 + '@hookform/resolvers@5.1.1': + resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==} + peerDependencies: + react-hook-form: ^7.55.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -4039,65 +4044,65 @@ packages: '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - '@tailwindcss/node@4.1.8': - resolution: {integrity: sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==} + '@tailwindcss/node@4.1.10': + resolution: {integrity: sha512-2ACf1znY5fpRBwRhMgj9ZXvb2XZW8qs+oTfotJ2C5xR0/WNL7UHZ7zXl6s+rUqedL1mNi+0O+WQr5awGowS3PQ==} - '@tailwindcss/oxide-android-arm64@4.1.8': - resolution: {integrity: sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==} + '@tailwindcss/oxide-android-arm64@4.1.10': + resolution: {integrity: sha512-VGLazCoRQ7rtsCzThaI1UyDu/XRYVyH4/EWiaSX6tFglE+xZB5cvtC5Omt0OQ+FfiIVP98su16jDVHDEIuH4iQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.8': - resolution: {integrity: sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==} + '@tailwindcss/oxide-darwin-arm64@4.1.10': + resolution: {integrity: sha512-ZIFqvR1irX2yNjWJzKCqTCcHZbgkSkSkZKbRM3BPzhDL/18idA8uWCoopYA2CSDdSGFlDAxYdU2yBHwAwx8euQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.8': - resolution: {integrity: sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==} + '@tailwindcss/oxide-darwin-x64@4.1.10': + resolution: {integrity: sha512-eCA4zbIhWUFDXoamNztmS0MjXHSEJYlvATzWnRiTqJkcUteSjO94PoRHJy1Xbwp9bptjeIxxBHh+zBWFhttbrQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.8': - resolution: {integrity: sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==} + '@tailwindcss/oxide-freebsd-x64@4.1.10': + resolution: {integrity: sha512-8/392Xu12R0cc93DpiJvNpJ4wYVSiciUlkiOHOSOQNH3adq9Gi/dtySK7dVQjXIOzlpSHjeCL89RUUI8/GTI6g==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': - resolution: {integrity: sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10': + resolution: {integrity: sha512-t9rhmLT6EqeuPT+MXhWhlRYIMSfh5LZ6kBrC4FS6/+M1yXwfCtp24UumgCWOAJVyjQwG+lYva6wWZxrfvB+NhQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': - resolution: {integrity: sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.10': + resolution: {integrity: sha512-3oWrlNlxLRxXejQ8zImzrVLuZ/9Z2SeKoLhtCu0hpo38hTO2iL86eFOu4sVR8cZc6n3z7eRXXqtHJECa6mFOvA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.8': - resolution: {integrity: sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.10': + resolution: {integrity: sha512-saScU0cmWvg/Ez4gUmQWr9pvY9Kssxt+Xenfx1LG7LmqjcrvBnw4r9VjkFcqmbBb7GCBwYNcZi9X3/oMda9sqQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.8': - resolution: {integrity: sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.10': + resolution: {integrity: sha512-/G3ao/ybV9YEEgAXeEg28dyH6gs1QG8tvdN9c2MNZdUXYBaIY/Gx0N6RlJzfLy/7Nkdok4kaxKPHKJUlAaoTdA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.8': - resolution: {integrity: sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==} + '@tailwindcss/oxide-linux-x64-musl@4.1.10': + resolution: {integrity: sha512-LNr7X8fTiKGRtQGOerSayc2pWJp/9ptRYAa4G+U+cjw9kJZvkopav1AQc5HHD+U364f71tZv6XamaHKgrIoVzA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.8': - resolution: {integrity: sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==} + '@tailwindcss/oxide-wasm32-wasi@4.1.10': + resolution: {integrity: sha512-d6ekQpopFQJAcIK2i7ZzWOYGZ+A6NzzvQ3ozBvWFdeyqfOZdYHU66g5yr+/HC4ipP1ZgWsqa80+ISNILk+ae/Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -4108,24 +4113,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': - resolution: {integrity: sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.10': + resolution: {integrity: sha512-i1Iwg9gRbwNVOCYmnigWCCgow8nDWSFmeTUU5nbNx3rqbe4p0kRbEqLwLJbYZKmSSp23g4N6rCDmm7OuPBXhDA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.8': - resolution: {integrity: sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.10': + resolution: {integrity: sha512-sGiJTjcBSfGq2DVRtaSljq5ZgZS2SDHSIfhOylkBvHVjwOsodBhnb3HdmiKkVuUGKD0I7G63abMOVaskj1KpOA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.8': - resolution: {integrity: sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==} + '@tailwindcss/oxide@4.1.10': + resolution: {integrity: sha512-v0C43s7Pjw+B9w21htrQwuFObSkio2aV/qPx/mhrRldbqxbWJK6KizM+q7BF1/1CmuLqZqX3CeYF7s7P9fbA8Q==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.1.8': - resolution: {integrity: sha512-vB/vlf7rIky+w94aWMw34bWW1ka6g6C3xIOdICKX2GC0VcLtL6fhlLiafF0DVIwa9V6EHz8kbWMkS2s2QvvNlw==} + '@tailwindcss/postcss@4.1.10': + resolution: {integrity: sha512-B+7r7ABZbkXJwpvt2VMnS6ujcDoR2OOcFaqrLIo1xbcdxje4Vf+VgJdBzNNbrAjBj/rLZ66/tlQ1knIGNLKOBQ==} '@tanstack/query-core@5.76.0': resolution: {integrity: sha512-FN375hb8ctzfNAlex5gHI6+WDXTNpe0nbxp/d2YJtnP+IBM6OUm7zcaoCW6T63BawGOYZBbKC0iPvr41TteNVg==} @@ -4283,6 +4288,12 @@ packages: '@types/node@22.15.30': resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==} + '@types/node@22.15.32': + resolution: {integrity: sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==} + + '@types/node@24.0.3': + resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==} + '@types/nodemailer@6.4.17': resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} @@ -4606,6 +4617,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -4713,8 +4729,8 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} - axios@1.9.0: - resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + axios@1.10.0: + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -4782,8 +4798,8 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001721: - resolution: {integrity: sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==} + caniuse-lite@1.0.30001723: + resolution: {integrity: sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -5136,8 +5152,8 @@ packages: duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - electron-to-chromium@1.5.165: - resolution: {integrity: sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==} + electron-to-chromium@1.5.168: + resolution: {integrity: sha512-RUNQmFLNIWVW6+z32EJQ5+qx8ci6RGvdtDC0Ls+F89wz6I2AthpXF0w0DIrn2jpLX0/PU9ZCo+Qp7bg/EckJmA==} emery@1.4.4: resolution: {integrity: sha512-mMoO3uGDoiw/DmZ/YekT9gEoC0IFAXNWzYVukY8+/j0Wt8un1IDraIYGx+cMbRh+fHaCDE6Ui7zFAN8ezZSsAA==} @@ -5148,8 +5164,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} enhanced-resolve@5.18.1: resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} @@ -6679,8 +6695,8 @@ packages: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.4: - resolution: {integrity: sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -6815,8 +6831,8 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} @@ -6857,6 +6873,12 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-hook-form@7.58.0: + resolution: {integrity: sha512-zGijmEed35oNfOfy7ub99jfjkiLhHwA3dl5AgyKdWC6QQzhnc7tkWewSa+T+A2EpLrc6wo5DUoZctS9kufWJjA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-i18next@15.5.2: resolution: {integrity: sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==} peerDependencies: @@ -6873,6 +6895,22 @@ packages: typescript: optional: true + react-i18next@15.5.3: + resolution: {integrity: sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -7255,8 +7293,8 @@ packages: stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} - supabase@2.24.3: - resolution: {integrity: sha512-mh9pi4C5pM159GyYE+Rv9qL8kg1kqzimQ4FBr6UH/WhzB/VcDaA+vpn/VeRC3CGVKr+d89Ra5MjDWg+/UoPpXg==} + supabase@2.26.9: + resolution: {integrity: sha512-wHl7HtAD2iHMVXL8JZyfSjdI0WYM7EF0ydThp1tSvDANaD2JHCZc8GH1NdzglbwGqdHmjCYeSZ+H28fmucYl7Q==} engines: {npm: '>=8'} hasBin: true @@ -7287,17 +7325,20 @@ packages: tailwind-merge@3.3.0: resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwindcss-animate@1.0.7: resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} peerDependencies: tailwindcss: '>=3.0.0 || insiders' + tailwindcss@4.1.10: + resolution: {integrity: sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA==} + tailwindcss@4.1.7: resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} - tailwindcss@4.1.8: - resolution: {integrity: sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==} - tapable@2.2.2: resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} engines: {node: '>=6'} @@ -7322,8 +7363,8 @@ packages: uglify-js: optional: true - terser@5.41.0: - resolution: {integrity: sha512-H406eLPXpZbAX14+B8psIuvIr8+3c+2hkuYzpMkoE0ij+NdsVATbA78vb8neA/eqrj7rywa2pIkdmWRsXW6wmw==} + terser@5.42.0: + resolution: {integrity: sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==} engines: {node: '>=10'} hasBin: true @@ -7470,6 +7511,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + undici@6.21.3: resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} engines: {node: '>=18.17'} @@ -7729,6 +7773,9 @@ packages: zod@3.25.56: resolution: {integrity: sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==} + zod@3.25.67: + resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -7857,7 +7904,7 @@ snapshots: '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) '@trpc/server': 11.3.1(typescript@5.8.3) '@types/aws-lambda': 8.10.149 - axios: 1.9.0 + axios: 1.10.0 flat: 6.0.1 undici: 6.21.3 transitivePeerDependencies: @@ -8073,6 +8120,11 @@ snapshots: '@standard-schema/utils': 0.3.0 react-hook-form: 7.57.0(react@19.1.0) + '@hookform/resolvers@5.1.1(react-hook-form@7.58.0(react@19.1.0))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.58.0(react@19.1.0) + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -11021,7 +11073,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@tailwindcss/node@4.1.8': + '@tailwindcss/node@4.1.10': dependencies: '@ampproject/remapping': 2.3.0 enhanced-resolve: 5.18.1 @@ -11029,69 +11081,69 @@ snapshots: lightningcss: 1.30.1 magic-string: 0.30.17 source-map-js: 1.2.1 - tailwindcss: 4.1.8 + tailwindcss: 4.1.10 - '@tailwindcss/oxide-android-arm64@4.1.8': + '@tailwindcss/oxide-android-arm64@4.1.10': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.8': + '@tailwindcss/oxide-darwin-arm64@4.1.10': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.8': + '@tailwindcss/oxide-darwin-x64@4.1.10': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.8': + '@tailwindcss/oxide-freebsd-x64@4.1.10': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.10': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.8': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.10': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.8': + '@tailwindcss/oxide-linux-arm64-musl@4.1.10': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.8': + '@tailwindcss/oxide-linux-x64-gnu@4.1.10': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.8': + '@tailwindcss/oxide-linux-x64-musl@4.1.10': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.8': + '@tailwindcss/oxide-wasm32-wasi@4.1.10': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.8': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.10': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.8': + '@tailwindcss/oxide-win32-x64-msvc@4.1.10': optional: true - '@tailwindcss/oxide@4.1.8': + '@tailwindcss/oxide@4.1.10': dependencies: detect-libc: 2.0.4 tar: 7.4.3 optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.8 - '@tailwindcss/oxide-darwin-arm64': 4.1.8 - '@tailwindcss/oxide-darwin-x64': 4.1.8 - '@tailwindcss/oxide-freebsd-x64': 4.1.8 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.8 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.8 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.8 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.8 - '@tailwindcss/oxide-linux-x64-musl': 4.1.8 - '@tailwindcss/oxide-wasm32-wasi': 4.1.8 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.8 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.8 + '@tailwindcss/oxide-android-arm64': 4.1.10 + '@tailwindcss/oxide-darwin-arm64': 4.1.10 + '@tailwindcss/oxide-darwin-x64': 4.1.10 + '@tailwindcss/oxide-freebsd-x64': 4.1.10 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.10 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.10 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.10 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.10 + '@tailwindcss/oxide-linux-x64-musl': 4.1.10 + '@tailwindcss/oxide-wasm32-wasi': 4.1.10 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.10 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.10 - '@tailwindcss/postcss@4.1.8': + '@tailwindcss/postcss@4.1.10': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.8 - '@tailwindcss/oxide': 4.1.8 - postcss: 8.5.4 - tailwindcss: 4.1.8 + '@tailwindcss/node': 4.1.10 + '@tailwindcss/oxide': 4.1.10 + postcss: 8.5.6 + tailwindcss: 4.1.10 '@tanstack/query-core@5.76.0': {} @@ -11156,7 +11208,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.3 '@types/d3-array@3.2.1': {} @@ -11236,15 +11288,23 @@ snapshots: '@types/mysql@2.15.26': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.3 '@types/node@22.15.30': dependencies: undici-types: 6.21.0 + '@types/node@22.15.32': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.0.3': + dependencies: + undici-types: 7.8.0 + '@types/nodemailer@6.4.17': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.3 '@types/parse-json@4.0.2': {} @@ -11254,7 +11314,7 @@ snapshots: '@types/pg@8.6.1': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.3 pg-protocol: 1.10.0 pg-types: 2.2.0 @@ -11272,7 +11332,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.3 '@types/unist@2.0.11': {} @@ -11280,7 +11340,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 22.15.30 + '@types/node': 22.15.32 '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: @@ -11609,9 +11669,9 @@ snapshots: '@xtuc/long@4.2.2': {} - acorn-import-assertions@1.9.0(acorn@8.14.1): + acorn-import-assertions@1.9.0(acorn@8.15.0): dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn-import-attributes@1.9.5(acorn@8.14.1): dependencies: @@ -11621,12 +11681,18 @@ snapshots: dependencies: acorn: 8.14.1 + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-walk@8.3.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn@8.14.1: {} + acorn@8.15.0: {} + agent-base@6.0.2: dependencies: debug: 4.4.1 @@ -11760,7 +11826,7 @@ snapshots: axe-core@4.10.3: {} - axios@1.9.0: + axios@1.10.0: dependencies: follow-redirects: 1.15.9 form-data: 4.0.3 @@ -11809,8 +11875,8 @@ snapshots: browserslist@4.25.0: dependencies: - caniuse-lite: 1.0.30001721 - electron-to-chromium: 1.5.165 + caniuse-lite: 1.0.30001723 + electron-to-chromium: 1.5.168 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.25.0) @@ -11842,11 +11908,11 @@ snapshots: caniuse-api@3.0.0: dependencies: browserslist: 4.25.0 - caniuse-lite: 1.0.30001721 + caniuse-lite: 1.0.30001723 lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001721: {} + caniuse-lite@1.0.30001723: {} ccount@2.0.1: {} @@ -11974,9 +12040,9 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-declaration-sorter@7.2.0(postcss@8.5.4): + css-declaration-sorter@7.2.0(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 css-select@5.1.0: dependencies: @@ -12000,49 +12066,49 @@ snapshots: cssesc@3.0.0: {} - cssnano-preset-default@7.0.7(postcss@8.5.4): + cssnano-preset-default@7.0.7(postcss@8.5.6): dependencies: browserslist: 4.25.0 - css-declaration-sorter: 7.2.0(postcss@8.5.4) - cssnano-utils: 5.0.1(postcss@8.5.4) - postcss: 8.5.4 - postcss-calc: 10.1.1(postcss@8.5.4) - postcss-colormin: 7.0.3(postcss@8.5.4) - postcss-convert-values: 7.0.5(postcss@8.5.4) - postcss-discard-comments: 7.0.4(postcss@8.5.4) - postcss-discard-duplicates: 7.0.2(postcss@8.5.4) - postcss-discard-empty: 7.0.1(postcss@8.5.4) - postcss-discard-overridden: 7.0.1(postcss@8.5.4) - postcss-merge-longhand: 7.0.5(postcss@8.5.4) - postcss-merge-rules: 7.0.5(postcss@8.5.4) - postcss-minify-font-values: 7.0.1(postcss@8.5.4) - postcss-minify-gradients: 7.0.1(postcss@8.5.4) - postcss-minify-params: 7.0.3(postcss@8.5.4) - postcss-minify-selectors: 7.0.5(postcss@8.5.4) - postcss-normalize-charset: 7.0.1(postcss@8.5.4) - postcss-normalize-display-values: 7.0.1(postcss@8.5.4) - postcss-normalize-positions: 7.0.1(postcss@8.5.4) - postcss-normalize-repeat-style: 7.0.1(postcss@8.5.4) - postcss-normalize-string: 7.0.1(postcss@8.5.4) - postcss-normalize-timing-functions: 7.0.1(postcss@8.5.4) - postcss-normalize-unicode: 7.0.3(postcss@8.5.4) - postcss-normalize-url: 7.0.1(postcss@8.5.4) - postcss-normalize-whitespace: 7.0.1(postcss@8.5.4) - postcss-ordered-values: 7.0.2(postcss@8.5.4) - postcss-reduce-initial: 7.0.3(postcss@8.5.4) - postcss-reduce-transforms: 7.0.1(postcss@8.5.4) - postcss-svgo: 7.0.2(postcss@8.5.4) - postcss-unique-selectors: 7.0.4(postcss@8.5.4) + css-declaration-sorter: 7.2.0(postcss@8.5.6) + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 + postcss-calc: 10.1.1(postcss@8.5.6) + postcss-colormin: 7.0.3(postcss@8.5.6) + postcss-convert-values: 7.0.5(postcss@8.5.6) + postcss-discard-comments: 7.0.4(postcss@8.5.6) + postcss-discard-duplicates: 7.0.2(postcss@8.5.6) + postcss-discard-empty: 7.0.1(postcss@8.5.6) + postcss-discard-overridden: 7.0.1(postcss@8.5.6) + postcss-merge-longhand: 7.0.5(postcss@8.5.6) + postcss-merge-rules: 7.0.5(postcss@8.5.6) + postcss-minify-font-values: 7.0.1(postcss@8.5.6) + postcss-minify-gradients: 7.0.1(postcss@8.5.6) + postcss-minify-params: 7.0.3(postcss@8.5.6) + postcss-minify-selectors: 7.0.5(postcss@8.5.6) + postcss-normalize-charset: 7.0.1(postcss@8.5.6) + postcss-normalize-display-values: 7.0.1(postcss@8.5.6) + postcss-normalize-positions: 7.0.1(postcss@8.5.6) + postcss-normalize-repeat-style: 7.0.1(postcss@8.5.6) + postcss-normalize-string: 7.0.1(postcss@8.5.6) + postcss-normalize-timing-functions: 7.0.1(postcss@8.5.6) + postcss-normalize-unicode: 7.0.3(postcss@8.5.6) + postcss-normalize-url: 7.0.1(postcss@8.5.6) + postcss-normalize-whitespace: 7.0.1(postcss@8.5.6) + postcss-ordered-values: 7.0.2(postcss@8.5.6) + postcss-reduce-initial: 7.0.3(postcss@8.5.6) + postcss-reduce-transforms: 7.0.1(postcss@8.5.6) + postcss-svgo: 7.0.2(postcss@8.5.6) + postcss-unique-selectors: 7.0.4(postcss@8.5.6) - cssnano-utils@5.0.1(postcss@8.5.4): + cssnano-utils@5.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 - cssnano@7.0.7(postcss@8.5.4): + cssnano@7.0.7(postcss@8.5.6): dependencies: - cssnano-preset-default: 7.0.7(postcss@8.5.4) + cssnano-preset-default: 7.0.7(postcss@8.5.6) lilconfig: 3.1.3 - postcss: 8.5.4 + postcss: 8.5.6 csso@5.0.5: dependencies: @@ -12203,7 +12269,7 @@ snapshots: duplexer@0.1.2: {} - electron-to-chromium@1.5.165: {} + electron-to-chromium@1.5.168: {} emery@1.4.4: {} @@ -12211,7 +12277,7 @@ snapshots: emoji-regex@9.2.2: {} - end-of-stream@1.4.4: + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -12861,8 +12927,8 @@ snapshots: import-in-the-middle@1.7.1: dependencies: - acorn: 8.14.1 - acorn-import-assertions: 1.9.0(acorn@8.14.1) + acorn: 8.15.0 + acorn-import-assertions: 1.9.0(acorn@8.15.0) cjs-module-lexer: 1.4.3 module-details-from-path: 1.0.4 @@ -13050,7 +13116,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.3 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -13436,8 +13502,8 @@ snapshots: micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) micromark-extension-mdx-expression: 3.0.1 micromark-extension-mdx-jsx: 3.0.2 micromark-extension-mdx-md: 2.0.0 @@ -13651,7 +13717,7 @@ snapshots: '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001721 + caniuse-lite: 1.0.30001723 postcss: 8.4.31 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) @@ -13851,7 +13917,7 @@ snapshots: minimist: 1.2.8 on-exit-leak-free: 2.1.2 pino-abstract-transport: 2.0.0 - pump: 3.0.2 + pump: 3.0.3 secure-json-parse: 2.7.0 sonic-boom: 4.2.0 strip-json-comments: 3.1.1 @@ -13874,142 +13940,142 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-calc@10.1.1(postcss@8.5.4): + postcss-calc@10.1.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - postcss-colormin@7.0.3(postcss@8.5.4): + postcss-colormin@7.0.3(postcss@8.5.6): dependencies: browserslist: 4.25.0 caniuse-api: 3.0.0 colord: 2.9.3 - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-convert-values@7.0.5(postcss@8.5.4): + postcss-convert-values@7.0.5(postcss@8.5.6): dependencies: browserslist: 4.25.0 - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-discard-comments@7.0.4(postcss@8.5.4): + postcss-discard-comments@7.0.4(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-selector-parser: 7.1.0 - postcss-discard-duplicates@7.0.2(postcss@8.5.4): + postcss-discard-duplicates@7.0.2(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 - postcss-discard-empty@7.0.1(postcss@8.5.4): + postcss-discard-empty@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 - postcss-discard-overridden@7.0.1(postcss@8.5.4): + postcss-discard-overridden@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 - postcss-merge-longhand@7.0.5(postcss@8.5.4): + postcss-merge-longhand@7.0.5(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - stylehacks: 7.0.5(postcss@8.5.4) + stylehacks: 7.0.5(postcss@8.5.6) - postcss-merge-rules@7.0.5(postcss@8.5.4): + postcss-merge-rules@7.0.5(postcss@8.5.6): dependencies: browserslist: 4.25.0 caniuse-api: 3.0.0 - cssnano-utils: 5.0.1(postcss@8.5.4) - postcss: 8.5.4 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 postcss-selector-parser: 7.1.0 - postcss-minify-font-values@7.0.1(postcss@8.5.4): + postcss-minify-font-values@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-minify-gradients@7.0.1(postcss@8.5.4): + postcss-minify-gradients@7.0.1(postcss@8.5.6): dependencies: colord: 2.9.3 - cssnano-utils: 5.0.1(postcss@8.5.4) - postcss: 8.5.4 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-minify-params@7.0.3(postcss@8.5.4): + postcss-minify-params@7.0.3(postcss@8.5.6): dependencies: browserslist: 4.25.0 - cssnano-utils: 5.0.1(postcss@8.5.4) - postcss: 8.5.4 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-minify-selectors@7.0.5(postcss@8.5.4): + postcss-minify-selectors@7.0.5(postcss@8.5.6): dependencies: cssesc: 3.0.0 - postcss: 8.5.4 + postcss: 8.5.6 postcss-selector-parser: 7.1.0 - postcss-normalize-charset@7.0.1(postcss@8.5.4): + postcss-normalize-charset@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 - postcss-normalize-display-values@7.0.1(postcss@8.5.4): + postcss-normalize-display-values@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-positions@7.0.1(postcss@8.5.4): + postcss-normalize-positions@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-repeat-style@7.0.1(postcss@8.5.4): + postcss-normalize-repeat-style@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-string@7.0.1(postcss@8.5.4): + postcss-normalize-string@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-timing-functions@7.0.1(postcss@8.5.4): + postcss-normalize-timing-functions@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-unicode@7.0.3(postcss@8.5.4): + postcss-normalize-unicode@7.0.3(postcss@8.5.6): dependencies: browserslist: 4.25.0 - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-url@7.0.1(postcss@8.5.4): + postcss-normalize-url@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-normalize-whitespace@7.0.1(postcss@8.5.4): + postcss-normalize-whitespace@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-ordered-values@7.0.2(postcss@8.5.4): + postcss-ordered-values@7.0.2(postcss@8.5.6): dependencies: - cssnano-utils: 5.0.1(postcss@8.5.4) - postcss: 8.5.4 + cssnano-utils: 5.0.1(postcss@8.5.6) + postcss: 8.5.6 postcss-value-parser: 4.2.0 - postcss-reduce-initial@7.0.3(postcss@8.5.4): + postcss-reduce-initial@7.0.3(postcss@8.5.6): dependencies: browserslist: 4.25.0 caniuse-api: 3.0.0 - postcss: 8.5.4 + postcss: 8.5.6 - postcss-reduce-transforms@7.0.1(postcss@8.5.4): + postcss-reduce-transforms@7.0.1(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 postcss-selector-parser@7.1.0: @@ -14017,15 +14083,15 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-svgo@7.0.2(postcss@8.5.4): + postcss-svgo@7.0.2(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-value-parser: 4.2.0 svgo: 3.3.2 - postcss-unique-selectors@7.0.4(postcss@8.5.4): + postcss-unique-selectors@7.0.4(postcss@8.5.6): dependencies: - postcss: 8.5.4 + postcss: 8.5.6 postcss-selector-parser: 7.1.0 postcss-value-parser@4.2.0: {} @@ -14036,7 +14102,7 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.4: + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -14136,14 +14202,14 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 22.15.30 + '@types/node': 24.0.3 long: 5.3.2 proxy-from-env@1.1.0: {} - pump@3.0.2: + pump@3.0.3: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 once: 1.4.0 punycode@2.3.1: {} @@ -14179,6 +14245,10 @@ snapshots: dependencies: react: 19.1.0 + react-hook-form@7.58.0(react@19.1.0): + dependencies: + react: 19.1.0 + react-i18next@15.5.2(i18next@25.1.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3): dependencies: '@babel/runtime': 7.27.6 @@ -14189,6 +14259,16 @@ snapshots: react-dom: 19.1.0(react@19.1.0) typescript: 5.8.3 + react-i18next@15.5.3(i18next@25.1.3(typescript@5.8.3))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3): + dependencies: + '@babel/runtime': 7.27.6 + html-parse-stringify: 3.0.1 + i18next: 25.1.3(typescript@5.8.3) + react: 19.1.0 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + typescript: 5.8.3 + react-is@16.13.1: {} react-is@18.3.1: {} @@ -14647,11 +14727,11 @@ snapshots: strip-json-comments@3.1.1: {} - stripe@18.2.1(@types/node@22.15.30): + stripe@18.2.1(@types/node@24.0.3): dependencies: qs: 6.14.0 optionalDependencies: - '@types/node': 22.15.30 + '@types/node': 24.0.3 strnum@2.1.1: {} @@ -14662,15 +14742,15 @@ snapshots: optionalDependencies: '@babel/core': 7.27.4 - stylehacks@7.0.5(postcss@8.5.4): + stylehacks@7.0.5(postcss@8.5.6): dependencies: browserslist: 4.25.0 - postcss: 8.5.4 + postcss: 8.5.6 postcss-selector-parser: 7.1.0 stylis@4.2.0: {} - supabase@2.24.3: + supabase@2.26.9: dependencies: bin-links: 5.0.0 https-proxy-agent: 7.0.6 @@ -14705,13 +14785,15 @@ snapshots: tailwind-merge@3.3.0: {} + tailwind-merge@3.3.1: {} + tailwindcss-animate@1.0.7(tailwindcss@4.1.7): dependencies: tailwindcss: 4.1.7 - tailwindcss@4.1.7: {} + tailwindcss@4.1.10: {} - tailwindcss@4.1.8: {} + tailwindcss@4.1.7: {} tapable@2.2.2: {} @@ -14730,13 +14812,13 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.2 serialize-javascript: 6.0.2 - terser: 5.41.0 + terser: 5.42.0 webpack: 5.99.9 - terser@5.41.0: + terser@5.42.0: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -14773,15 +14855,15 @@ snapshots: ts-case-convert@2.1.0: {} - ts-node@10.9.2(@types/node@22.15.30)(typescript@5.8.3): + ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.15.30 - acorn: 8.14.1 + '@types/node': 22.15.32 + acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 @@ -14889,6 +14971,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.8.0: {} + undici@6.21.3: {} unist-util-is@6.0.0: @@ -14916,7 +15000,7 @@ snapshots: unplugin@1.0.1: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 chokidar: 3.6.0 webpack-sources: 3.3.2 webpack-virtual-modules: 0.5.0 @@ -15024,7 +15108,7 @@ snapshots: webpack-bundle-analyzer@4.10.1: dependencies: '@discoveryjs/json-ext': 0.5.7 - acorn: 8.14.1 + acorn: 8.15.0 acorn-walk: 8.3.4 commander: 7.2.0 debounce: 1.2.1 @@ -15052,7 +15136,7 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.1 + acorn: 8.15.0 browserslist: 4.25.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 @@ -15207,4 +15291,6 @@ snapshots: zod@3.25.56: {} + zod@3.25.67: {} + zwitch@2.0.4: {} diff --git a/supabase/database.types.ts b/supabase/database.types.ts index 3671308..81b8777 100644 --- a/supabase/database.types.ts +++ b/supabase/database.types.ts @@ -48,6 +48,48 @@ export type Database = { } Relationships: [] } + request_entries: { + Row: { + comment: string | null + created_at: string + id: number + personal_code: number | null + request_api: string + request_api_method: string + requested_end_date: string | null + requested_start_date: string | null + service_id: number | null + service_provider_id: number | null + status: Database["audit"]["Enums"]["request_status"] + } + Insert: { + comment?: string | null + created_at?: string + id?: number + personal_code?: number | null + request_api: string + request_api_method: string + requested_end_date?: string | null + requested_start_date?: string | null + service_id?: number | null + service_provider_id?: number | null + status: Database["audit"]["Enums"]["request_status"] + } + Update: { + comment?: string | null + created_at?: string + id?: number + personal_code?: number | null + request_api?: string + request_api_method?: string + requested_end_date?: string | null + requested_start_date?: string | null + service_id?: number | null + service_provider_id?: number | null + status?: Database["audit"]["Enums"]["request_status"] + } + Relationships: [] + } sync_entries: { Row: { changed_by_role: string @@ -83,6 +125,7 @@ export type Database = { [_ in never]: never } Enums: { + request_status: "SUCCESS" | "FAIL" sync_status: "SUCCESS" | "FAIL" } CompositeTypes: { @@ -596,6 +639,158 @@ export type Database = { } Relationships: [] } + connected_online_providers: { + Row: { + can_select_worker: boolean + created_at: string + email: string | null + id: number + name: string + personal_code_required: boolean + phone_number: string | null + updated_at: string | null + } + Insert: { + can_select_worker: boolean + created_at?: string + email?: string | null + id: number + name: string + personal_code_required: boolean + phone_number?: string | null + updated_at?: string | null + } + Update: { + can_select_worker?: boolean + created_at?: string + email?: string | null + id?: number + name?: string + personal_code_required?: boolean + phone_number?: string | null + updated_at?: string | null + } + Relationships: [] + } + connected_online_reservation: { + Row: { + booking_code: string + clinic_id: number + comments: string | null + created_at: string + discount_code: string | null + id: number + lang: string + requires_payment: boolean + service_id: number + service_user_id: number | null + start_time: string + sync_user_id: number + updated_at: string | null + user_id: string + } + Insert: { + booking_code: string + clinic_id: number + comments?: string | null + created_at?: string + discount_code?: string | null + id?: number + lang: string + requires_payment: boolean + service_id: number + service_user_id?: number | null + start_time: string + sync_user_id: number + updated_at?: string | null + user_id: string + } + Update: { + booking_code?: string + clinic_id?: number + comments?: string | null + created_at?: string + discount_code?: string | null + id?: number + lang?: string + requires_payment?: boolean + service_id?: number + service_user_id?: number | null + start_time?: string + sync_user_id?: number + updated_at?: string | null + user_id?: string + } + Relationships: [] + } + connected_online_services: { + Row: { + clinic_id: number + code: string + created_at: string + description: string | null + display: string | null + duration: number + has_free_codes: boolean + id: number + name: string + neto_duration: number | null + online_hide_duration: number | null + online_hide_price: number | null + price: number + price_periods: string | null + requires_payment: boolean + sync_id: number + updated_at: string | null + } + Insert: { + clinic_id: number + code: string + created_at?: string + description?: string | null + display?: string | null + duration: number + has_free_codes: boolean + id: number + name: string + neto_duration?: number | null + online_hide_duration?: number | null + online_hide_price?: number | null + price: number + price_periods?: string | null + requires_payment: boolean + sync_id: number + updated_at?: string | null + } + Update: { + clinic_id?: number + code?: string + created_at?: string + description?: string | null + display?: string | null + duration?: number + has_free_codes?: boolean + id?: number + name?: string + neto_duration?: number | null + online_hide_duration?: number | null + online_hide_price?: number | null + price?: number + price_periods?: string | null + requires_payment?: boolean + sync_id?: number + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "connected_online_services_clinic_id_fkey" + columns: ["clinic_id"] + isOneToOne: false + referencedRelation: "connected_online_providers" + referencedColumns: ["id"] + }, + ] + } invitations: { Row: { account_id: string @@ -661,6 +856,63 @@ export type Database = { }, ] } + nonces: { + Row: { + client_token: string + created_at: string + expires_at: string + id: string + last_verification_at: string | null + last_verification_ip: unknown | null + last_verification_user_agent: string | null + metadata: Json | null + nonce: string + purpose: string + revoked: boolean + revoked_reason: string | null + scopes: string[] | null + used_at: string | null + user_id: string | null + verification_attempts: number + } + Insert: { + client_token: string + created_at?: string + expires_at: string + id?: string + last_verification_at?: string | null + last_verification_ip?: unknown | null + last_verification_user_agent?: string | null + metadata?: Json | null + nonce: string + purpose: string + revoked?: boolean + revoked_reason?: string | null + scopes?: string[] | null + used_at?: string | null + user_id?: string | null + verification_attempts?: number + } + Update: { + client_token?: string + created_at?: string + expires_at?: string + id?: string + last_verification_at?: string | null + last_verification_ip?: unknown | null + last_verification_user_agent?: string | null + metadata?: Json | null + nonce?: string + purpose?: string + revoked?: boolean + revoked_reason?: string | null + scopes?: string[] | null + used_at?: string | null + user_id?: string | null + verification_attempts?: number + } + Relationships: [] + } notifications: { Row: { account_id: string @@ -1058,6 +1310,17 @@ export type Database = { updated_at: string } } + create_nonce: { + Args: { + p_user_id?: string + p_purpose?: string + p_expires_in_seconds?: number + p_metadata?: Json + p_scopes?: string[] + p_revoke_previous?: boolean + } + Returns: Json + } create_team_account: { Args: { account_name: string } Returns: { @@ -1110,6 +1373,10 @@ export type Database = { Args: Record Returns: Json } + get_nonce_status: { + Args: { p_id: string } + Returns: Json + } get_upper_system_role: { Args: Record Returns: string @@ -1146,6 +1413,10 @@ export type Database = { } Returns: boolean } + is_aal2: { + Args: Record + Returns: boolean + } is_account_owner: { Args: { account_id: string } Returns: boolean @@ -1154,14 +1425,26 @@ export type Database = { Args: { target_account_id: string } Returns: boolean } + is_mfa_compliant: { + Args: Record + Returns: boolean + } is_set: { Args: { field_name: string } Returns: boolean } + is_super_admin: { + Args: Record + Returns: boolean + } is_team_member: { Args: { account_id: string; user_id: string } Returns: boolean } + revoke_nonce: { + Args: { p_id: string; p_reason?: string } + Returns: boolean + } team_account_workspace: { Args: { account_slug: string } Returns: { @@ -1236,6 +1519,18 @@ export type Database = { updated_at: string } } + verify_nonce: { + Args: { + p_token: string + p_purpose: string + p_user_id?: string + p_required_scopes?: string[] + p_max_verification_attempts?: number + p_ip?: unknown + p_user_agent?: string + } + Returns: Json + } } Enums: { analysis_order_status: @@ -1383,6 +1678,7 @@ export type CompositeTypes< export const Constants = { audit: { Enums: { + request_status: ["SUCCESS", "FAIL"], sync_status: ["SUCCESS", "FAIL"], }, }, diff --git a/supabase/migrations/20250616142604_add_connected_online_tables.sql b/supabase/migrations/20250616142604_add_connected_online_tables.sql new file mode 100644 index 0000000..74f701a --- /dev/null +++ b/supabase/migrations/20250616142604_add_connected_online_tables.sql @@ -0,0 +1,227 @@ +create table "public"."connected_online_providers" ( + "id" bigint not null, + "name" text not null, + "email" text, + "phone_number" text, + "can_select_worker" boolean not null, + "personal_code_required" boolean not null, + "created_at" timestamp with time zone not null default now(), + "updated_at" timestamp without time zone default now() +); + + +alter table "public"."connected_online_providers" enable row level security; + +create table "public"."connected_online_services" ( + "id" bigint not null, + "clinic_id" bigint not null, + "sync_id" bigint not null, + "name" text not null, + "description" text, + "price" double precision not null, + "requires_payment" boolean not null, + "duration" bigint not null, + "neto_duration" bigint, + "display" text, + "price_periods" text, + "online_hide_duration" bigint, + "online_hide_price" bigint, + "code" text not null, + "has_free_codes" boolean not null, + "created_at" timestamp with time zone not null default now(), + "updated_at" timestamp with time zone default now() +); + + +alter table "public"."connected_online_services" enable row level security; + +CREATE UNIQUE INDEX connected_online_providers_id_key ON public.connected_online_providers USING btree (id); + +CREATE UNIQUE INDEX connected_online_providers_pkey ON public.connected_online_providers USING btree (id); + +CREATE UNIQUE INDEX connected_online_services_id_key ON public.connected_online_services USING btree (id); + +CREATE UNIQUE INDEX connected_online_services_pkey ON public.connected_online_services USING btree (id); + +alter table "public"."connected_online_providers" add constraint "connected_online_providers_pkey" PRIMARY KEY using index "connected_online_providers_pkey"; + +alter table "public"."connected_online_services" add constraint "connected_online_services_pkey" PRIMARY KEY using index "connected_online_services_pkey"; + +alter table "public"."connected_online_providers" add constraint "connected_online_providers_id_key" UNIQUE using index "connected_online_providers_id_key"; + +alter table "public"."connected_online_services" add constraint "connected_online_services_clinic_id_fkey" FOREIGN KEY (clinic_id) REFERENCES connected_online_providers(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."connected_online_services" validate constraint "connected_online_services_clinic_id_fkey"; + +alter table "public"."connected_online_services" add constraint "connected_online_services_id_key" UNIQUE using index "connected_online_services_id_key"; + +grant delete on table "public"."connected_online_providers" to "service_role"; + +grant insert on table "public"."connected_online_providers" to "service_role"; + +grant references on table "public"."connected_online_providers" to "service_role"; + +grant select on table "public"."connected_online_providers" to "service_role"; + +grant trigger on table "public"."connected_online_providers" to "service_role"; + +grant truncate on table "public"."connected_online_providers" to "service_role"; + +grant update on table "public"."connected_online_providers" to "service_role"; + +grant select on table "public"."connected_online_providers" to "authenticated"; + +grant delete on table "public"."connected_online_services" to "service_role"; + +grant insert on table "public"."connected_online_services" to "service_role"; + +grant references on table "public"."connected_online_services" to "service_role"; + +grant select on table "public"."connected_online_services" to "service_role"; + +grant trigger on table "public"."connected_online_services" to "service_role"; + +grant truncate on table "public"."connected_online_services" to "service_role"; + +grant update on table "public"."connected_online_services" to "service_role"; + +grant select on table "public"."connected_online_services" to "authenticated"; + +create type "audit"."request_status" as enum ('SUCCESS', 'FAIL'); + +create table "audit"."request_entries" ( + "id" bigint generated by default as identity not null, + "personal_code" bigint, + "request_api" text not null, + "request_api_method" text not null, + "status" audit.request_status not null, + "comment" text, + "service_provider_id" bigint, + "service_id" bigint, + "requested_start_date" timestamp with time zone, + "requested_end_date" timestamp with time zone, + "created_at" timestamp with time zone not null default now() +); + + +alter table "audit"."request_entries" enable row level security; + +CREATE UNIQUE INDEX request_entries_pkey ON audit.request_entries USING btree (id); + +alter table "audit"."request_entries" add constraint "request_entries_pkey" PRIMARY KEY using index "request_entries_pkey"; + +grant delete on table "audit"."request_entries" to "service_role"; + +grant insert on table "audit"."request_entries" to "service_role"; + +grant references on table "audit"."request_entries" to "service_role"; + +grant select on table "audit"."request_entries" to "service_role"; + +grant trigger on table "audit"."request_entries" to "service_role"; + +grant truncate on table "audit"."request_entries" to "service_role"; + +grant update on table "audit"."request_entries" to "service_role"; + +create policy "service_role_all" +on "audit"."request_entries" +as permissive +for all +to service_role +using (true); + +create table "public"."connected_online_reservation" ( + "id" bigint generated by default as identity not null, + "user_id" uuid not null, + "booking_code" text not null, + "service_id" bigint not null, + "clinic_id" bigint not null, + "service_user_id" bigint, + "sync_user_id" bigint not null, + "requires_payment" boolean not null, + "comments" text, + "start_time" timestamp with time zone not null, + "lang" text not null, + "discount_code" text, + "created_at" timestamp with time zone not null default now(), + "updated_at" timestamp with time zone default now() +); + + +alter table "public"."connected_online_reservation" enable row level security; + +CREATE UNIQUE INDEX connected_online_reservation_booking_code_key ON public.connected_online_reservation USING btree (booking_code); + +CREATE UNIQUE INDEX connected_online_reservation_pkey ON public.connected_online_reservation USING btree (id); + +alter table "public"."connected_online_reservation" add constraint "connected_online_reservation_pkey" PRIMARY KEY using index "connected_online_reservation_pkey"; + +alter table "public"."connected_online_reservation" add constraint "connected_online_reservation_booking_code_key" UNIQUE using index "connected_online_reservation_booking_code_key"; + +alter table "public"."connected_online_reservation" add constraint "connected_online_reservation_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."connected_online_reservation" validate constraint "connected_online_reservation_user_id_fkey"; + +grant delete on table "public"."connected_online_reservation" to "service_role"; + +grant insert on table "public"."connected_online_reservation" to "service_role"; + +grant references on table "public"."connected_online_reservation" to "service_role"; + +grant select on table "public"."connected_online_reservation" to "service_role"; + +grant trigger on table "public"."connected_online_reservation" to "service_role"; + +grant truncate on table "public"."connected_online_reservation" to "service_role"; + +grant update on table "public"."connected_online_reservation" to "service_role"; + +create policy "service_role_all" +on "public"."connected_online_reservation" +as permissive +for all +to service_role +using (true); + + +CREATE TRIGGER connected_online_providers_change_record_timestamps AFTER INSERT OR UPDATE ON public.connected_online_providers FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamps(); + +CREATE TRIGGER connected_online_services_change_record_timestamps AFTER INSERT OR UPDATE ON public.connected_online_services FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamps(); + +create policy "service_role_all" +on "public"."connected_online_providers" +as permissive +for all +to service_role +using (true); + + +create policy "service_role_all" +on "public"."connected_online_services" +as permissive +for all +to service_role +using (true); + +create policy "authenticated_select" +on "public"."connected_online_providers" +as permissive +for select +to authenticated +using (true); + +create policy "authenticated_select" +on "public"."connected_online_services" +as permissive +for select +to authenticated +using (true); + + +create policy "own_all" +on "public"."connected_online_reservation" +as permissive +for all +to authenticated +using ((( SELECT auth.uid() AS uid) = user_id)); \ No newline at end of file From ea540a4aeb3e6f77b05589f08e2f9d7349fac346 Mon Sep 17 00:00:00 2001 From: Helena <37183360+helenarebane@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:40:22 +0300 Subject: [PATCH 2/2] B2B-51: add medreport product tables, relations and constraints (#19) * B2B-51: add medreport product tables, relations and constraints * change table names * sync data based on env --------- Co-authored-by: Helena --- jobs/sync-connected-online.ts | 31 ++- ...619070038_add_medreport_product_tables.sql | 225 ++++++++++++++++++ 2 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 supabase/migrations/20250619070038_add_medreport_product_tables.sql diff --git a/jobs/sync-connected-online.ts b/jobs/sync-connected-online.ts index b655d86..12e93fc 100644 --- a/jobs/sync-connected-online.ts +++ b/jobs/sync-connected-online.ts @@ -8,6 +8,8 @@ async function syncData() { config({ path: `.env.${process.env.NODE_ENV}` }); } + const isProd = process.env.NODE_ENV === 'production'; + const baseUrl = process.env.CONNECTED_ONLINE_URL; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseServiceRoleKey = @@ -26,15 +28,12 @@ async function syncData() { }); try { - const response = await axios.post( - `${baseUrl}/Search_Load`, - { - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - param: "{'Value':'|et|-1'}", // get all available services in Estonian + const response = await axios.post(`${baseUrl}/Search_Load`, { + headers: { + 'Content-Type': 'application/json; charset=utf-8', }, - ); + param: "{'Value':'|et|-1'}", // get all available services in Estonian + }); const responseData: { Value: any; @@ -59,8 +58,20 @@ async function syncData() { }); } - const clinics = responseData.Data.T_Lic; - const services = responseData.Data.T_Service; + let clinics; + let services; + // Filter out "Dentas Demo OÜ" in prod or only sync "Dentas Demo OÜ" in any other environment + if (isProd) { + clinics = responseData.Data.T_Lic.filter((clinic) => clinic.ID !== 2); + services = responseData.Data.T_Service.filter( + (service) => service.ClinicID !== 2, + ); + } else { + clinics = responseData.Data.T_Lic.filter((clinic) => clinic.ID === 2); + services = responseData.Data.T_Service.filter( + (service) => service.ClinicID === 2, + ); + } const mappedClinics = clinics.map((clinic) => { return { diff --git a/supabase/migrations/20250619070038_add_medreport_product_tables.sql b/supabase/migrations/20250619070038_add_medreport_product_tables.sql new file mode 100644 index 0000000..938fe6e --- /dev/null +++ b/supabase/migrations/20250619070038_add_medreport_product_tables.sql @@ -0,0 +1,225 @@ + +create table "public"."medreport_product_groups" ( + "id" bigint generated by default as identity not null, + "name" text not null, + "created_at" timestamp with time zone not null default now(), + "updated_at" timestamp with time zone +); + +create table "public"."medreport_products" ( + "id" bigint generated by default as identity not null, + "name" text not null, + "product_group_id" bigint, + "created_at" timestamp with time zone not null default now(), + "updated_at" timestamp with time zone default now() +); + + +alter table "public"."medreport_products" enable row level security; + +create table "public"."medreport_products_analyses_relations" ( + "product_id" bigint not null, + "analysis_element_id" bigint, + "analysis_id" bigint +); + +alter table "public"."medreport_product_groups" enable row level security; + +alter table "public"."medreport_products_analyses_relations" enable row level security; + +CREATE UNIQUE INDEX medreport_product_groups_name_key ON public.medreport_product_groups USING btree (name); + +CREATE UNIQUE INDEX medreport_product_groups_pkey ON public.medreport_product_groups USING btree (id); + +alter table "public"."medreport_product_groups" add constraint "medreport_product_groups_pkey" PRIMARY KEY using index "medreport_product_groups_pkey"; + +alter table "public"."medreport_product_groups" add constraint "medreport_product_groups_name_key" UNIQUE using index "medreport_product_groups_name_key"; + +alter table "public"."medreport_products" add constraint "medreport_products_product_groups_id_fkey" FOREIGN KEY (product_group_id) REFERENCES medreport_product_groups(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."medreport_products" validate constraint "medreport_products_product_groups_id_fkey"; + +grant select on table "public"."medreport_product_groups" to "anon"; + +grant select on table "public"."medreport_product_groups" to "authenticated"; + +grant delete on table "public"."medreport_product_groups" to "service_role"; + +grant insert on table "public"."medreport_product_groups" to "service_role"; + +grant references on table "public"."medreport_product_groups" to "service_role"; + +grant select on table "public"."medreport_product_groups" to "service_role"; + +grant trigger on table "public"."medreport_product_groups" to "service_role"; + +grant truncate on table "public"."medreport_product_groups" to "service_role"; + +grant update on table "public"."medreport_product_groups" to "service_role"; + +CREATE UNIQUE INDEX medreport_products_analyses_analysis_element_id_key ON public.medreport_products_analyses_relations USING btree (analysis_element_id); + +CREATE UNIQUE INDEX medreport_products_analyses_analysis_id_key ON public.medreport_products_analyses_relations USING btree (analysis_id); + +CREATE UNIQUE INDEX medreport_products_analyses_pkey ON public.medreport_products_analyses_relations USING btree (product_id); + +CREATE UNIQUE INDEX medreport_products_name_key ON public.medreport_products USING btree (name); + +CREATE UNIQUE INDEX medreport_products_pkey ON public.medreport_products USING btree (id); + +alter table "public"."medreport_products" add constraint "medreport_products_pkey" PRIMARY KEY using index "medreport_products_pkey"; + +alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_pkey" PRIMARY KEY using index "medreport_products_analyses_pkey"; + +alter table "public"."medreport_products" add constraint "medreport_products_name_key" UNIQUE using index "medreport_products_name_key"; + +alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_analysis_element_id_fkey" FOREIGN KEY (analysis_element_id) REFERENCES analysis_elements(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."medreport_products_analyses_relations" validate constraint "medreport_products_analyses_analysis_element_id_fkey"; + +alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_analysis_element_id_key" UNIQUE using index "medreport_products_analyses_analysis_element_id_key"; + +alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_analysis_id_fkey" FOREIGN KEY (analysis_id) REFERENCES analyses(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."medreport_products_analyses_relations" validate constraint "medreport_products_analyses_analysis_id_fkey"; + +alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_analysis_id_key" UNIQUE using index "medreport_products_analyses_analysis_id_key"; + +alter table "public"."medreport_products_analyses_relations" add constraint "medreport_products_analyses_product_id_fkey" FOREIGN KEY (product_id) REFERENCES medreport_products(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."medreport_products_analyses_relations" validate constraint "medreport_products_analyses_product_id_fkey"; + +alter table "public"."medreport_products_analyses_relations" add constraint "product_can_be_tied_to_only_one_external_item" CHECK (((analysis_id IS NULL) OR (analysis_element_id IS NULL))) not valid; + +alter table "public"."medreport_products_analyses_relations" validate constraint "product_can_be_tied_to_only_one_external_item"; + +grant select on table "public"."medreport_products" to "anon"; + +grant select on table "public"."medreport_products" to "authenticated"; + +grant delete on table "public"."medreport_products" to "service_role"; + +grant insert on table "public"."medreport_products" to "service_role"; + +grant references on table "public"."medreport_products" to "service_role"; + +grant select on table "public"."medreport_products" to "service_role"; + +grant trigger on table "public"."medreport_products" to "service_role"; + +grant truncate on table "public"."medreport_products" to "service_role"; + +grant update on table "public"."medreport_products" to "service_role"; + +grant select on table "public"."medreport_products_analyses_relations" to "anon"; + +grant select on table "public"."medreport_products_analyses_relations" to "authenticated"; + +grant delete on table "public"."medreport_products_analyses_relations" to "service_role"; + +grant insert on table "public"."medreport_products_analyses_relations" to "service_role"; + +grant references on table "public"."medreport_products_analyses_relations" to "service_role"; + +grant select on table "public"."medreport_products_analyses_relations" to "service_role"; + +grant trigger on table "public"."medreport_products_analyses_relations" to "service_role"; + +grant truncate on table "public"."medreport_products_analyses_relations" to "service_role"; + +grant update on table "public"."medreport_products_analyses_relations" to "service_role"; + +create policy "Enable read access for all users" +on "public"."medreport_products_analyses_relations" +as permissive +for select +to public +using (true); + + +ALTER TABLE medreport_products_analyses_relations +ADD CONSTRAINT product_can_be_tied_to_only_one_analysis_item +CHECK (analysis_id IS NULL OR analysis_element_id IS NULL); + + +create table "public"."medreport_products_external_services_relations" ( + "product_id" bigint not null, + "connected_online_service_id" bigint not null +); + +alter table "public"."medreport_products_external_services_relations" enable row level security; + +CREATE UNIQUE INDEX medreport_products_connected_online_services_id_key ON public.medreport_products_external_services_relations USING btree (connected_online_service_id); + +CREATE UNIQUE INDEX medreport_products_connected_online_services_pkey ON public.medreport_products_external_services_relations USING btree (connected_online_service_id); + +alter table "public"."medreport_products_external_services_relations" add constraint "medreport_products_connected_online_services_pkey" PRIMARY KEY using index "medreport_products_connected_online_services_pkey"; + +alter table "public"."medreport_products_external_services_relations" add constraint "medreport_products_connected_online_services_id_fkey" FOREIGN KEY (connected_online_service_id) REFERENCES connected_online_services(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."medreport_products_external_services_relations" validate constraint "medreport_products_connected_online_services_id_fkey"; + +alter table "public"."medreport_products_external_services_relations" add constraint "medreport_products_connected_online_services_id_key" UNIQUE using index "medreport_products_connected_online_services_id_key"; + +alter table "public"."medreport_products_external_services_relations" add constraint "medreport_products_connected_online_services_product_id_fkey" FOREIGN KEY (product_id) REFERENCES medreport_products(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."medreport_products_external_services_relations" validate constraint "medreport_products_connected_online_services_product_id_fkey"; + +grant select on table "public"."medreport_products_external_services_relations" to "anon"; + +grant select on table "public"."medreport_products_external_services_relations" to "authenticated"; + +grant delete on table "public"."medreport_products_external_services_relations" to "service_role"; + +grant insert on table "public"."medreport_products_external_services_relations" to "service_role"; + +grant references on table "public"."medreport_products_external_services_relations" to "service_role"; + +grant select on table "public"."medreport_products_external_services_relations" to "service_role"; + +grant trigger on table "public"."medreport_products_external_services_relations" to "service_role"; + +grant truncate on table "public"."medreport_products_external_services_relations" to "service_role"; + +grant update on table "public"."medreport_products_external_services_relations" to "service_role"; + +CREATE OR REPLACE FUNCTION check_tied_to_connected_online() +RETURNS TRIGGER AS $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM medreport_products_external_services_relations + WHERE product_id = NEW.product_id + ) THEN + RAISE EXCEPTION 'Value "%" already exists in medreport_products_external_services_relations', NEW.product_id; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION check_tied_to_analysis_item() +RETURNS TRIGGER AS $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM medreport_products_analyses_relations + WHERE product_id = NEW.product_id + ) THEN + RAISE EXCEPTION 'Value "%" already exists in medreport_products_analyses_relations', NEW.product_id; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER check_not_already_tied_to_connected_online BEFORE INSERT OR UPDATE ON public.medreport_products_analyses_relations FOR EACH ROW EXECUTE FUNCTION check_tied_to_connected_online(); + +CREATE TRIGGER check_not_already_tied_to_analysis BEFORE INSERT OR UPDATE ON public.medreport_products_external_services_relations FOR EACH ROW EXECUTE FUNCTION check_tied_to_analysis_item(); + +create policy "read_all" +on "public"."medreport_product_groups" +as permissive +for select +to public +using (true);