diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 92d857181..613c5ec82 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -1035,6 +1035,7 @@ - use_fippa - use_paint_scale_data - uselocalmediaserver + - external_shop_id - website - workingdays - zip_post @@ -1130,6 +1131,7 @@ - use_fippa - use_paint_scale_data - uselocalmediaserver + - external_shop_id - website - workingdays - zip_post diff --git a/hasura/migrations/1749499292683_alter_table_public_bodyshops_add_column_we_profile_id/down.sql b/hasura/migrations/1749499292683_alter_table_public_bodyshops_add_column_we_profile_id/down.sql new file mode 100644 index 000000000..e3d2f88eb --- /dev/null +++ b/hasura/migrations/1749499292683_alter_table_public_bodyshops_add_column_we_profile_id/down.sql @@ -0,0 +1,4 @@ +-- Could not auto-generate a down migration. +-- Please write an appropriate down migration for the SQL below: +-- alter table "public"."bodyshops" add column "we_profile_id" text +-- null; diff --git a/hasura/migrations/1749499292683_alter_table_public_bodyshops_add_column_we_profile_id/up.sql b/hasura/migrations/1749499292683_alter_table_public_bodyshops_add_column_we_profile_id/up.sql new file mode 100644 index 000000000..f8fc3a654 --- /dev/null +++ b/hasura/migrations/1749499292683_alter_table_public_bodyshops_add_column_we_profile_id/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add column "we_profile_id" text + null; diff --git a/hasura/migrations/1749505657175_alter_table_public_bodyshops_alter_column_we_profile_id/down.sql b/hasura/migrations/1749505657175_alter_table_public_bodyshops_alter_column_we_profile_id/down.sql new file mode 100644 index 000000000..6c5fb479a --- /dev/null +++ b/hasura/migrations/1749505657175_alter_table_public_bodyshops_alter_column_we_profile_id/down.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" rename column "parts_management_key" to "we_profile_id"; +alter table "public"."bodyshops" drop constraint "bodyshops_we_profile_id_key"; diff --git a/hasura/migrations/1749505657175_alter_table_public_bodyshops_alter_column_we_profile_id/up.sql b/hasura/migrations/1749505657175_alter_table_public_bodyshops_alter_column_we_profile_id/up.sql new file mode 100644 index 000000000..8c84ea5f5 --- /dev/null +++ b/hasura/migrations/1749505657175_alter_table_public_bodyshops_alter_column_we_profile_id/up.sql @@ -0,0 +1,2 @@ +alter table "public"."bodyshops" add constraint "bodyshops_we_profile_id_key" unique ("we_profile_id"); +alter table "public"."bodyshops" rename column "we_profile_id" to "parts_management_key"; diff --git a/hasura/migrations/1749507125123_alter_table_public_bodyshops_alter_column_parts_management_key/down.sql b/hasura/migrations/1749507125123_alter_table_public_bodyshops_alter_column_parts_management_key/down.sql new file mode 100644 index 000000000..a809155d5 --- /dev/null +++ b/hasura/migrations/1749507125123_alter_table_public_bodyshops_alter_column_parts_management_key/down.sql @@ -0,0 +1 @@ +alter table "public"."bodyshops" rename column "external_shop_id" to "parts_management_key"; diff --git a/hasura/migrations/1749507125123_alter_table_public_bodyshops_alter_column_parts_management_key/up.sql b/hasura/migrations/1749507125123_alter_table_public_bodyshops_alter_column_parts_management_key/up.sql new file mode 100644 index 000000000..2127b4003 --- /dev/null +++ b/hasura/migrations/1749507125123_alter_table_public_bodyshops_alter_column_parts_management_key/up.sql @@ -0,0 +1 @@ +alter table "public"."bodyshops" rename column "parts_management_key" to "external_shop_id"; diff --git a/server/integrations/partsManagement/defaultNewShop.json b/server/integrations/partsManagement/defaultNewShop.json new file mode 100644 index 000000000..281f31dbb --- /dev/null +++ b/server/integrations/partsManagement/defaultNewShop.json @@ -0,0 +1,1236 @@ +{ + "associations": { + "data": [ + { + "useremail": "linda@imex.prod", + "active": false, + "authlevel": 99 + }, + { + "useremail": "patrick@imex.prod", + "active": false, + "authlevel": 99 + }, + { + "useremail": "allan@imex.prod", + "active": false, + "authlevel": 99 + }, + { + "useremail": "logan@imex.prod", + "active": false, + "authlevel": 99 + }, + { + "useremail": "nicolette@imex.prod", + "active": false, + "authlevel": 99 + } + ] + }, + "logo_img_path": { + "src": "", + "width": "", + "height": "", + "headerMargin": "135" + }, + "md_ro_statuses": { + "statuses": [ + "Open", + "Scheduled", + "Arrived", + "Hold", + "Parts", + "Body", + "Prep", + "Paint", + "Reassembly", + "Sublet", + "Detail", + "Completed", + "Delivered", + "Invoiced", + "Exported", + "Void" + ], + "default_void": "Void", + "active_statuses": [ + "Open", + "Scheduled", + "Arrived", + "Hold", + "Parts", + "Body", + "Prep", + "Paint", + "Reassembly", + "Sublet", + "Detail", + "Completed", + "Delivered" + ], + "default_arrived": "Arrived", + "default_exported": "Exported", + "default_imported": "Open", + "default_invoiced": "Invoiced", + "default_completed": "Completed", + "default_delivered": "Delivered", + "default_scheduled": "Scheduled", + "production_statuses": [ + "Arrived", + "Hold", + "Parts", + "Body", + "Prep", + "Paint", + "Reassembly", + "Sublet", + "Detail", + "Completed" + ], + "pre_production_statuses": [ + "Open", + "Scheduled" + ], + "post_production_statuses": [ + "Delivered", + "Invoiced", + "Exported" + ] + }, + "md_order_statuses": { + "default_bo": "Backordered", + "default_ordered": "Ordered", + "default_received": "Received", + "default_returned": "Returned" + }, + "shoprates": { + "rate_ats": 8.68, + "rate_ats_flat": 100.0 + }, + "md_responsibility_centers": { + "ar": { + "accountname": "ACCOUNTS RECEIVABLE" + }, + "costs": [ + { + "name": "Aftermarket", + "accountdesc": "Aftermarket", + "accountitem": "Aftermarket", + "accountname": "BODY SHOP COST:PARTS:AFTERMARKET", + "accountnumber": "Aftermarket" + }, + { + "name": "ATP", + "accountdesc": "ATP", + "accountitem": "BODY SHOP_ATP", + "accountname": "BODY SHOP COST:ATP", + "accountnumber": "ATP" + }, + { + "name": "Body", + "accountdesc": "Body", + "accountitem": "BODY SHOP_LAB", + "accountname": "BODY SHOP COST:LABOR:BODY", + "accountnumber": "BODY" + }, + { + "name": "DETAIL", + "accountdesc": "Detailing", + "accountitem": "Detail", + "accountname": "BODY SHOP COST:LABOR:DETAIL", + "accountnumber": "DETAIL" + }, + { + "name": "Diagnostic", + "accountdesc": "Diagnostic", + "accountitem": "BODY SHOP_LAB", + "accountname": "BODY SHOP COST:SUBLET", + "accountnumber": "SUBLET" + }, + { + "name": "Electrical", + "accountdesc": "Electrical", + "accountitem": "Electrical", + "accountname": "BODY SHOP COST:SUBLET", + "accountnumber": "Electrical" + }, + { + "name": "Chrome", + "accountdesc": "Chrome", + "accountitem": "Chrome", + "accountname": "BODY SHOP COST:PARTS:AFTERMARKET", + "accountnumber": "Aftermarket" + }, + { + "name": "Frame", + "accountdesc": "Frame", + "accountitem": "Frame", + "accountname": "BODY SHOP COST:LABOR:FRAME", + "accountnumber": "Frame" + }, + { + "name": "Mechanical", + "accountdesc": "Mechanical", + "accountitem": "Mechanical", + "accountname": "BODY SHOP COST:LABOR:MECHANICAL", + "accountnumber": "Mechanical" + }, + { + "name": "Refinish", + "accountdesc": "Refinish", + "accountitem": "BODY SHOP_LAR", + "accountname": "BODY SHOP COST:LABOR:REFINISH", + "accountnumber": "REFINISH" + }, + { + "name": "Structural", + "accountdesc": "Structural", + "accountitem": "Structural", + "accountname": "BODY SHOP COST:SUBLET", + "accountnumber": "SUBLET" + }, + { + "name": "Existing", + "accountdesc": "Existing", + "accountitem": "Existing", + "accountname": "BODY SHOP COST:PARTS:OTHER", + "accountnumber": "Existing" + }, + { + "name": "Glass", + "accountdesc": "Glass", + "accountitem": "Glass", + "accountname": "BODY SHOP COST:PARTS:GLASS", + "accountnumber": "Glass" + }, + { + "name": "LKQ", + "accountdesc": "LKQ", + "accountitem": "LKQ", + "accountname": "BODY SHOP COST:PARTS:LKQ", + "accountnumber": "LKQ" + }, + { + "name": "OEM", + "accountdesc": "OEM", + "accountitem": "OEM", + "accountname": "BODY SHOP COST:PARTS:OEM", + "accountnumber": "OEM" + }, + { + "name": "OEM Partial", + "accountdesc": "Partial", + "accountitem": "Partial", + "accountname": "BODY SHOP COST:PARTS:OEM", + "accountnumber": "OEM " + }, + { + "name": "Re-cored", + "accountdesc": "cored", + "accountitem": "cored", + "accountname": "BODY SHOP COST:PARTS:AFTERMARKET", + "accountnumber": "Aftermarket" + }, + { + "name": "Remanufactured", + "accountdesc": "Remanufactured", + "accountitem": "Remanufactured", + "accountname": "BODY SHOP COST:PARTS:LKQ", + "accountnumber": "Remanufactured" + }, + { + "name": "Other", + "accountdesc": "Other", + "accountitem": "Other", + "accountname": "BODY SHOP COST:PARTS:OTHER", + "accountnumber": "OTHER" + }, + { + "name": "Sublet", + "accountdesc": "Sublet to Other", + "accountitem": "Sublet", + "accountname": "BODY SHOP COST:SUBLET", + "accountnumber": "Sublet" + }, + { + "name": "Towing", + "accountdesc": "Towing", + "accountitem": "Towing", + "accountname": "BODY SHOP COST:TOWING", + "accountnumber": "Towing" + }, + { + "name": "Paint Materials", + "accountdesc": "PAINT MATERIALS", + "accountitem": "mat", + "accountname": "BODY SHOP COST:MATERIALS:PAINT", + "accountnumber": "PAINT" + }, + { + "name": "Shop Materials", + "accountdesc": "BODY MATERIALS", + "accountitem": "shop", + "accountname": "BODY SHOP COST:MATERIALS:BODY", + "accountnumber": "Shop" + }, + { + "name": "Levies", + "accountdesc": "Levies", + "accountitem": "Levies", + "accountname": "BODY SHOP COST:Levies (Tire And Battery)", + "accountnumber": "Levies" + }, + { + "name": "LA1", + "accountdesc": "LA1", + "accountname": "BODY SHOP COST:LABOR" + }, + { + "name": "LA2", + "accountdesc": "LA2", + "accountname": "BODY SHOP COST:LABOR" + }, + { + "name": "LA3", + "accountdesc": "LA3", + "accountname": "BODY SHOP COST:LABOR" + }, + { + "name": "LA4", + "accountdesc": "LA4", + "accountname": "BODY SHOP COST:LABOR" + }, + { + "name": "Sublet (L)", + "accountdesc": "Sublet Labor", + "accountname": "BODY SHOP COST:SUBLET" + }, + { + "name": "Aluminum", + "accountdesc": "Aluminum", + "accountname": "BODY SHOP COST:LABOR:BODY" + }, + { + "name": "Glass Labor", + "accountdesc": "Glass Labor", + "accountname": "BODY SHOP COST:LABOR:GLASS" + } + ], + "taxes": { + "local": { + "name": "n", + "rate": 0, + "accountdesc": "n", + "accountitem": "n" + }, + "state": { + "name": "PST on Sales", + "rate": 7, + "accountdesc": "PST on Sales", + "accountitem": "PST On Sales" + }, + "federal": { + "name": "GST on Sales", + "rate": 5, + "accountdesc": "GST on Sales", + "accountitem": "GST On Sales" + } + }, + "refund": { + "accountitem": "BODY SHOP_CUSTPAY" + }, + "profits": [ + { + "name": "Aftermarket", + "accountdesc": "Aftermarket", + "accountitem": "BODY SHOP_PAA", + "accountname": "Aftermarket", + "accountnumber": "Aftermarket" + }, + { + "name": "ATP", + "accountdesc": "ATP", + "accountitem": "BODY SHOP_ATP", + "accountname": "ATP", + "accountnumber": "ATP" + }, + { + "name": "Body", + "accountdesc": "Body Labour", + "accountitem": "BODY SHOP_LAB", + "accountname": "BODY SHOP SALES:LABOR:BODY", + "accountnumber": "Body" + }, + { + "name": "Detail", + "accountdesc": "Detail", + "accountitem": "BODY SHOP_LAU", + "accountname": "BODY SHOP SALES:LABOR:DETAIL", + "accountnumber": "Detail" + }, + { + "name": "Diagnostic", + "accountdesc": "Diagnostic", + "accountitem": "BODY SHOP_LAB", + "accountname": "BODY SHOP SALES:LABOR:BODY", + "accountnumber": "Body" + }, + { + "name": "Electrical", + "accountdesc": "Electrical", + "accountitem": "BODY SHOP_LAB", + "accountname": "BODY SHOP SALES:LABOR:BODY", + "accountnumber": "Body" + }, + { + "name": "Chrome", + "accountdesc": "Chrome", + "accountitem": "BODY SHOP_PAA", + "accountname": "Aftermarket", + "accountnumber": "Aftermarket" + }, + { + "name": "Frame", + "accountdesc": "Frame", + "accountitem": "BODY SHOP_LAF", + "accountname": "Frame", + "accountnumber": "Frame" + }, + { + "name": "Mechanical", + "accountdesc": "Mechanical", + "accountitem": "BODY SHOP_LAM", + "accountname": "Mechanical", + "accountnumber": "Mechanical" + }, + { + "name": "Refinish", + "accountdesc": "Refinish Labour", + "accountitem": "BODY SHOP_LAR", + "accountname": "BODY SHOP SALES:LABOR:REFINISH", + "accountnumber": "Refinish" + }, + { + "name": "Structural", + "accountdesc": "Structural", + "accountitem": "BODY SHOP_LAB", + "accountname": "Structural", + "accountnumber": "Structural" + }, + { + "name": "Existing", + "accountdesc": "Existing", + "accountitem": "BODY SHOP_PAO", + "accountname": "Existing", + "accountnumber": "Existing" + }, + { + "name": "Glass", + "accountdesc": "Glass", + "accountitem": "BODY SHOP_LAB", + "accountname": "Glass", + "accountnumber": "Glass" + }, + { + "name": "LKQ", + "accountdesc": "LKQ", + "accountitem": "BODY SHOP_PAL", + "accountname": "BODY SHOP SALES:PARTS:LKQ", + "accountnumber": "LKQ" + }, + { + "name": "OEM", + "accountdesc": "OEM Parts", + "accountitem": "BODY SHOP_PAN", + "accountname": "BODY SHOP SALES:PARTS:OEM", + "accountnumber": "OEM" + }, + { + "name": "OEM Partial", + "accountdesc": "OEM Partial", + "accountitem": "BODY SHOP_PAN", + "accountname": "OEM Partial", + "accountnumber": "OEM Partial" + }, + { + "name": "Re-cored", + "accountdesc": "Cored", + "accountitem": "BODY SHOP_PAO", + "accountname": "Re-cored", + "accountnumber": "Re-cored" + }, + { + "name": "Remanufactured", + "accountdesc": "Remanufactured", + "accountitem": "BODY SHOP_PAO", + "accountname": "Remanufactured", + "accountnumber": "Remanufactured" + }, + { + "name": "Other", + "accountdesc": "Other", + "accountitem": "BODY SHOP_PAO", + "accountname": "Other", + "accountnumber": "Other" + }, + { + "name": "Sublet", + "accountdesc": "Sublet", + "accountitem": "BODY SHOP_PAS", + "accountname": "BODY SHOP SALES:SUBLET", + "accountnumber": "BODY SHOP SALES:SUBLET" + }, + { + "name": "Towing", + "accountdesc": "Towing", + "accountitem": "BODY SHOP_TOW", + "accountname": "BODY SHOP SALES:TOWING", + "accountnumber": "BODY SHOP SALES:TOWING" + }, + { + "name": "Paint Materials", + "accountdesc": "Paint Material ", + "accountitem": "BODY SHOP_MAPA", + "accountname": "BODY SHOP SALES:MATERIALS:PAINT", + "accountnumber": "BODY SHOP SALES:MATERIALS:PAINT" + }, + { + "name": "Shop Materials", + "accountdesc": "Shop Material ", + "accountitem": "BODY SHOP_MASH", + "accountname": "BODY SHOP SALES:MATERIALS:SHOP", + "accountnumber": "BODY SHOP SALES:MATERIALS:SHOP" + }, + { + "name": "LA1", + "accountdesc": "LA1", + "accountitem": "BODY SHOP_LAB" + }, + { + "name": "LA2", + "accountdesc": "LA2", + "accountitem": "BODY SHOP_LAB" + }, + { + "name": "LA3", + "accountdesc": "LA3", + "accountitem": "BODY SHOP_LAB" + }, + { + "name": "LA4", + "accountdesc": "LA4", + "accountitem": "BODY SHOP_LAB" + }, + { + "name": "Sublet Labor", + "accountdesc": "PASL", + "accountitem": "BODY SHOP_PAS" + }, + { + "name": "Aluminum", + "accountdesc": "Aluminum", + "accountitem": "BODY SHOP_LAB" + }, + { + "name": "Adjustments", + "accountdesc": "Adjustments", + "accountitem": "BODY SHOP_ADJ" + }, + { + "name": "Glass Labor", + "accountdesc": "Glass Labor", + "accountitem": "BODY SHOP_LAG" + } + ], + "defaults": { + "costs": { + "ATS": "ATP", + "LA1": "LA1", + "LA2": "LA2", + "LA3": "LA3", + "LA4": "LA4", + "LAA": "Aluminum", + "LAB": "Body", + "LAD": "Diagnostic", + "LAE": "Electrical", + "LAF": "Frame", + "LAG": "Glass Labor", + "LAM": "Mechanical", + "LAR": "Refinish", + "LAS": "Structural", + "LAU": "DETAIL", + "PAA": "Aftermarket", + "PAC": "Chrome", + "PAG": "Glass", + "PAL": "LKQ", + "PAM": "Remanufactured", + "PAN": "OEM", + "PAO": "Other", + "PAP": "OEM Partial", + "PAR": "Re-cored", + "PAS": "Sublet", + "TOW": "Towing", + "MAPA": "Paint Materials", + "MASH": "Shop Materials", + "PASL": "Sublet (L)" + }, + "profits": { + "ATS": "ATP", + "LA1": "LA1", + "LA2": "LA2", + "LA3": "LA3", + "LA4": "LA4", + "LAA": "Body", + "LAB": "Body", + "LAD": "Diagnostic", + "LAE": "Electrical", + "LAF": "Frame", + "LAG": "Glass Labor", + "LAM": "Mechanical", + "LAR": "Refinish", + "LAS": "Structural", + "LAU": "Detail", + "PAA": "Aftermarket", + "PAC": "Chrome", + "PAG": "Glass", + "PAL": "LKQ", + "PAM": "Remanufactured", + "PAN": "OEM", + "PAO": "Other", + "PAP": "OEM Partial", + "PAR": "Re-cored", + "PAS": "Sublet", + "TOW": "Towing", + "MAPA": "Paint Materials", + "MASH": "Shop Materials", + "PASL": "Sublet Labor" + } + }, + "sales_tax_codes": [ + { + "code": "G", + "local": false, + "state": false, + "federal": true, + "description": "GST Only" + }, + { + "code": "S", + "state": true, + "federal": true, + "description": "Standard" + }, + { + "code": "E", + "local": false, + "state": false, + "federal": false, + "description": "Exempt" + } + ] + }, + "template_header": "", + "bill_tax_rates": { + "local_tax_rate": 0, + "state_tax_rate": 7, + "federal_tax_rate": 5 + }, + "accountingconfig": { + "tiers": 2, + "twotierpref": "source" + }, + "appt_length": 15, + "stripe_acct_id": "", + "ssbuckets": [ + { + "id": "express", + "lt": 3, + "gte": 0, + "label": "Express", + "target": 1 + }, + { + "id": "small", + "lt": 8, + "gte": 3, + "label": "Small", + "target": 4 + }, + { + "id": "medium", + "lt": 15, + "gte": 8, + "label": "Medium", + "target": 2 + }, + { + "id": "large", + "lt": 30, + "gte": 15, + "label": "Large", + "target": 1 + }, + { + "id": "heavy", + "lt": 999, + "gte": 30, + "label": "Heavy", + "target": 1 + } + ], + "scoreboard_target": { + "dailyBodyTarget": 80, + "dailyPaintTarget": 25, + "lastNumberWorkingDays": 12 + }, + "md_referral_sources": [ + "Friend", + "Word of Mouth", + "Google", + "ICBC" + ], + "md_messaging_presets": [ + { + "text": "Thanks for getting your car fixed with us.", + "label": "Thanks" + }, + { + "text": "Your appointment has been confirmed!", + "label": "Confirmation" + } + ], + "intakechecklist": { + "form": [ + { + "name": "Keys", + "type": "checkbox", + "label": "Keys", + "required": false + }, + { + "name": "Wheel Locks", + "type": "checkbox", + "label": "Wheel Locks", + "required": false + }, + { + "name": "Notes", + "type": "textarea", + "label": "Notes" + } + ], + "templates": [ + "worksheet_sorted_by_operation", + "fippa_authorization" + ] + }, + "speedprint": [ + { + "id": "New File", + "label": "New File", + "templates": [ + "coversheet_landscape", + "fippa_authorization", + "window_tag" + ] + }, + { + "id": "Final Paperwork", + "label": "Final Paperwork", + "templates": [ + "ro_totals", + "final_invoice" + ] + }, + { + "id": "Tech Paperwork", + "label": "Tech Paperwork", + "templates": [ + "worksheet_sorted_by_operation", + "supplement_request" + ] + } + ], + "md_parts_locations": [ + "Parts Room A", + "Parts Room B" + ], + "md_notes_presets": [ + { + "text": "CUSTOMER UPDATE:", + "label": "CUSTOMER UPDATE:" + } + ], + "md_rbac": { + "csi:page": 11, + "jobs:void": 80, + "shop:rbac": 99, + "bills:list": 11, + "bills:view": 11, + "csi:export": 11, + "jobs:admin": 80, + "jobs:close": 11, + "bills:enter": 11, + "jobs:create": 11, + "jobs:detail": 11, + "jobs:intake": 11, + "owners:list": 11, + "shop:config": 70, + "bills:delete": 11, + "jobs:deliver": 11, + "shop:vendors": 1, + "jobs:list-all": 11, + "owners:detail": 11, + "payments:list": 11, + "schedule:view": 11, + "bills:reexport": 11, + "contracts:list": 11, + "employees:page": 80, + "payments:enter": 11, + "phonebook:edit": 11, + "phonebook:view": 1, + "shop:dashboard": 80, + "jobs:list-ready": 11, + "jobs:partsqueue": 1, + "production:list": 1, + "scoreboard:view": 11, + "shiftclock:view": 1, + "contracts:create": 11, + "contracts:detail": 11, + "courtesycar:list": 11, + "jobs:list-active": 1, + "production:board": 1, + "timetickets:edit": 80, + "timetickets:list": 11, + "ttapprovals:view": 80, + "users:editaccess": 99, + "shop:reportcenter": 11, + "timetickets:enter": 1, + "courtesycar:create": 11, + "courtesycar:detail": 11, + "temporarydocs:view": 1, + "accounting:payables": 11, + "accounting:payments": 11, + "employee_teams:page": 80, + "jobs:available-list": 11, + "jobs:checklist-view": 11, + "ttapprovals:approve": 80, + "accounting:exportlog": 11, + "timetickets:shiftedit": 80, + "accounting:receivables": 11, + "timetickets:editcommitted": 80 + }, + "prodtargethrs": 105.0, + "md_classes": [], + "md_ins_cos": [ + { + "zip": "", + "city": "", + "name": "ICBC", + "state": "", + "street1": "" + }, + { + "zip": "", + "city": "", + "name": "ICBC-GLASS", + "state": "", + "street1": "" + }, + { + "zip": "", + "city": "", + "name": "PRIVATE", + "state": "", + "street1": "" + }, + { + "zip": "", + "city": "", + "name": "BCAA", + "state": "", + "street1": "" + }, + { + "zip": "", + "city": "", + "name": "ECONOMICAL", + "state": "", + "street1": "" + }, + { + "zip": "", + "city": "", + "name": "MPI", + "state": "", + "street1": "" + }, + { + "zip": "", + "city": "", + "name": "OPTIOM", + "state": "", + "street1": "" + }, + { + "zip": "", + "city": "", + "name": "SGI", + "state": "", + "street1": "" + }, + { + "zip": "", + "city": "", + "name": "WARRANTY", + "state": "", + "street1": "" + } + ], + "md_categories": [ + "Hit & Run", + "OEM only", + "Comp", + "Warranty" + ], + "enforce_class": false, + "md_labor_rates": [ + { + "label": "1", + "rate_la1": 0, + "rate_la2": 0, + "rate_la3": 0, + "rate_la4": 0, + "rate_laa": 0, + "rate_lab": 75.3, + "rate_lad": 0, + "rate_lae": 0, + "rate_laf": 86.07, + "rate_lag": 0, + "rate_lam": 94.39, + "rate_lar": 75.3, + "rate_las": 0, + "rate_ma2s": 0, + "rate_ma3s": 0, + "rate_mabl": 1, + "rate_macs": 1, + "rate_mahw": 0, + "rate_mapa": 45.15, + "rate_mash": 6.11, + "rate_matd": 0, + "rate_label": "PRIVATE" + } + ], + "deliverchecklist": { + "form": [ + { + "name": "Detailed", + "type": "checkbox", + "label": "Detailed?", + "required": false + }, + { + "name": "Post Scan", + "type": "checkbox", + "label": "Scanned?", + "required": false + }, + { + "name": "Comments", + "type": "text", + "label": "Additional Comments?", + "required": false + } + ], + "templates": [ + "ro_with_description", + "final_invoice", + "invoice_total_payable" + ], + "actual_delivery": true + }, + "target_touchtime": 3.0, + "appt_colors": [ + { + "color": { + "hex": "#e6b3e4", + "hsl": { + "a": 1, + "h": 301.8897637795275, + "l": 0.8, + "s": 0.4999999999999999 + }, + "hsv": { + "a": 1, + "h": 301.8897637795275, + "s": 0.22222222222222215, + "v": 0.9 + }, + "rgb": { + "a": 1, + "b": 228, + "g": 179, + "r": 230 + }, + "oldHue": 301.8897637795275, + "source": "hsl" + }, + "label": "EXPRESS 0-3H" + }, + { + "color": { + "hex": "#e5e6b3", + "hsl": { + "a": 1, + "h": 60.00000000000003, + "l": 0.8, + "s": 0.4999999999999999 + }, + "hsv": { + "a": 1, + "h": 60.00000000000003, + "s": 0.22222222222222215, + "v": 0.9 + }, + "rgb": { + "a": 1, + "b": 179, + "g": 230, + "r": 229 + }, + "oldHue": 60, + "source": "hsl" + }, + "label": "SMALL 3-8H" + }, + { + "color": { + "hex": "#40bf5e", + "hsl": { + "a": 1, + "h": 134.11764705882354, + "l": 0.5, + "s": 0.5 + }, + "hsv": { + "a": 1, + "h": 134.11764705882354, + "s": 0.6666666666666666, + "v": 0.75 + }, + "rgb": { + "a": 1, + "b": 94, + "g": 191, + "r": 64 + }, + "oldHue": 134.11764705882354, + "source": "hsl" + }, + "label": "MEDIUM 8-15H" + }, + { + "color": { + "hex": "#4085bf", + "hsl": { + "a": 1, + "h": 207.4015748031496, + "l": 0.5, + "s": 0.5 + }, + "hsv": { + "a": 1, + "h": 207.4015748031496, + "s": 0.6666666666666666, + "v": 0.75 + }, + "rgb": { + "a": 1, + "b": 191, + "g": 133, + "r": 64 + }, + "oldHue": 207.4015748031496, + "source": "hsl" + }, + "label": "LARGE 15-30H" + }, + { + "color": { + "hex": "#bf4068", + "hsl": { + "a": 1, + "h": 341.12359550561797, + "l": 0.5, + "s": 0.5 + }, + "hsv": { + "a": 1, + "h": 341.12359550561797, + "s": 0.6666666666666666, + "v": 0.75 + }, + "rgb": { + "a": 1, + "b": 104, + "g": 64, + "r": 191 + }, + "oldHue": 341.12359550561797, + "source": "hsl" + }, + "label": "HEAVY 30-999H" + }, + { + "color": { + "hex": "#b3e6e2", + "hsl": { + "a": 1, + "h": 175.95505617977528, + "l": 0.8, + "s": 0.4999999999999999 + }, + "hsv": { + "a": 1, + "h": 175.95505617977528, + "s": 0.22222222222222215, + "v": 0.9 + }, + "rgb": { + "a": 1, + "b": 226, + "g": 230, + "r": 179 + }, + "oldHue": 175.95505617977528, + "source": "hsl" + }, + "label": "EST" + }, + { + "color": { + "hex": "#3b2d86", + "hsl": { + "a": 1, + "h": 249.99999999999994, + "l": 0.35, + "s": 0.49999999999999983 + }, + "hsv": { + "a": 1, + "h": 249.99999999999994, + "s": 0.6666666666666665, + "v": 0.5249999999999999 + }, + "rgb": { + "a": 1, + "b": 134, + "g": 45, + "r": 59 + }, + "oldHue": 249.99999999999994, + "source": "hsl" + }, + "label": "OTHER APPT" + } + ], + "appt_alt_transport": [ + "No car", + "Rental", + "CC", + "Vehicle Pick up", + "Ride", + "Internal" + ], + "schedule_start_time": "2020-12-15T16:00:00+00:00", + "schedule_end_time": "2021-02-12T01:30:00+00:00", + "default_adjustment_rate": 0, + "workingdays": { + "friday": true, + "monday": true, + "sunday": false, + "tuesday": true, + "saturday": false, + "thursday": true, + "wednesday": true + }, + "use_fippa": false, + "md_payment_types": [ + "Cash", + "Cheque", + "Master Card", + "Visa", + "Debit", + "EFT", + "American Express" + ], + "md_hour_split": { + "prep": 0.0, + "paint": 0.0 + }, + "sub_status": "active", + "jobsizelimit": 104857600, + "md_ccc_rates": [], + "enforce_referral": false, + "last_name_first": true, + "jc_hourly_rates": { + "mapa": 33.0, + "mash": 4.2 + }, + "md_jobline_presets": [ + { + "label": "Glass Labour", + "line_desc": "Glass Labour", + "mod_lb_hrs": null, + "mod_lbr_ty": "LAG" + }, + { + "label": "Urethane", + "part_qty": 1, + "act_price": 48, + "line_desc": "Urethane", + "part_type": "PAL" + }, + { + "label": "Windshield OEM", + "part_qty": 1, + "part_type": "PAN" + }, + { + "label": "PDR", + "part_type": "PAS", + "oem_partno": "" + }, + { + "label": "Windshield Aftermarket", + "part_qty": 1, + "part_type": "PAA" + }, + { + "label": "Moulding", + "part_qty": 1, + "part_type": "PAA" + } + ], + "cdk_dealerid": null, + "features": { + "allAccess": true, + "singleDeviceOnly": false + }, + "attach_pdf_to_email": true, + "tt_allow_post_to_invoiced": true, + "cdk_configuration": null, + "md_estimators": [], + "md_ded_notes": [ + "Paid", + "Owning" + ], + "pbs_configuration": {}, + "pbs_serialnumber": null, + "md_filehandlers": [], + "md_email_cc": { + "parts_order": [] + }, + "timezone": "America/Vancouver" +} diff --git a/server/integrations/partsManagement/partsManagementProvisioning.js b/server/integrations/partsManagement/partsManagementProvisioning.js new file mode 100644 index 000000000..8e2213bdf --- /dev/null +++ b/server/integrations/partsManagement/partsManagementProvisioning.js @@ -0,0 +1,257 @@ +const crypto = require("crypto"); +const admin = require("firebase-admin"); +const client = require("../../graphql-client/graphql-client").client; +const DefaultNewShop = require("./defaultNewShop.json"); + +/** + * Ensures that the required fields are present in the payload. + * @param payload + * @param fields + */ +const requireFields = (payload, fields) => { + for (const field of fields) { + if (!payload[field]) { + throw { status: 400, message: `${field} is required.` }; + } + } +}; + +/** + * Ensures that the email is not already registered in Firebase. + * @param email + * @returns {Promise} + */ +const ensureEmailNotRegistered = async (email) => { + try { + await admin.auth().getUserByEmail(email); + throw { status: 400, message: "userEmail is already registered in Firebase." }; + } catch (err) { + if (err.code !== "auth/user-not-found") { + throw { status: 500, message: "Error validating userEmail uniqueness", detail: err }; + } + } +}; + +/** + * Creates a new Firebase user with the provided email. + * @param email + * @returns {Promise} + */ +const createFirebaseUser = async (email) => { + return admin.auth().createUser({ email }); +}; + +/** + * Deletes a Firebase user by their UID. + * @param uid + * @returns {Promise} + */ +const deleteFirebaseUser = async (uid) => { + return admin.auth().deleteUser(uid); +}; + +/** + * Generates a password reset link for the given email. + * @param email + * @returns {Promise} + */ +const generateResetLink = async (email) => { + return admin.auth().generatePasswordResetLink(email); +}; + +/** + * Ensures that the external shop ID is unique in the database. + * @param externalId + * @returns {Promise} + */ +const ensureExternalIdUnique = async (externalId) => { + const query = ` + query CHECK_KEY($key: String!) { + bodyshops(where: { external_shop_id: { _eq: $key } }) { + external_shop_id + } + }`; + const resp = await client.request(query, { key: externalId }); + if (resp.bodyshops.length) { + throw { status: 400, message: `external_shop_id '${externalId}' is already in use.` }; + } +}; + +/** + * Inserts a new bodyshop into the database. + * @param input + * @returns {Promise<*>} + */ +const insertBodyshop = async (input) => { + const mutation = ` + mutation CREATE_SHOP($bs: bodyshops_insert_input!) { + insert_bodyshops_one(object: $bs) { id } + }`; + const resp = await client.request(mutation, { bs: input }); + return resp.insert_bodyshops_one.id; +}; + +/** + * Deletes all vendors associated with a specific shop ID. + * @param shopId + * @returns {Promise} + */ +const deleteVendorsByShop = async (shopId) => { + const mutation = ` + mutation DELETE_VENDORS($shopId: uuid!) { + delete_vendors(where: { shopid: { _eq: $shopId } }) { + affected_rows + } + }`; + await client.request(mutation, { shopId }); +}; + +/** + * Deletes a bodyshop by its ID. + * @param shopId + * @returns {Promise} + */ +const deleteBodyshop = async (shopId) => { + const mutation = ` + mutation DELETE_SHOP($id: uuid!) { + delete_bodyshops_by_pk(id: $id) { id } + }`; + await client.request(mutation, { id: shopId }); +}; + +/** + * Inserts a new user association into the database. + * @param uid + * @param email + * @param shopId + * @returns {Promise<*>} + */ +const insertUserAssociation = async (uid, email, shopId) => { + const mutation = ` + mutation CREATE_USER($u: users_insert_input!) { + insert_users_one(object: $u) { + id: authid + email + } + }`; + const vars = { + u: { + email, + authid: uid, + validemail: true, + associations: { + data: [{ shopid: shopId, authlevel: 80, active: true }] + } + } + }; + const resp = await client.request(mutation, vars); + return resp.insert_users_one; +}; + +/** + * Handles the provisioning of a new parts management shop and user. + * @param req + * @param res + * @returns {Promise<*>} + */ +const partsManagementProvisioning = async (req, res) => { + const { logger } = req; + const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() }; + + try { + // Validate inputs + await ensureEmailNotRegistered(p.userEmail); + requireFields(p, [ + "external_shop_id", + "shopname", + "address1", + "city", + "state", + "zip_post", + "country", + "email", + "phone", + "userEmail" + ]); + await ensureExternalIdUnique(p.external_shop_id); + + logger.log("admin-create-shop-user", "debug", p.userEmail, null, { + request: req.body, + ioadmin: true + }); + + // Create shop + const shopInput = { + shopname: p.shopname, + address1: p.address1, + address2: p.address2 || null, + city: p.city, + state: p.state, + zip_post: p.zip_post, + country: p.country, + email: p.email, + external_shop_id: p.external_shop_id, + timezone: p.timezone, + phone: p.phone, + logo_img_path: { + src: p.logoUrl, + width: "", + height: "", + headerMargin: DefaultNewShop.logo_img_path.headerMargin + }, + md_ro_statuses: DefaultNewShop.md_ro_statuses, + vendors: { + data: p.vendors.map((v) => ({ + name: v.name, + street1: v.street1 || null, + street2: v.street2 || null, + city: v.city || null, + state: v.state || null, + zip: v.zip || null, + country: v.country || null, + email: v.email || null, + discount: v.discount ?? 0, + due_date: v.due_date ?? null, + cost_center: v.cost_center || null, + favorite: v.favorite ?? [], + phone: v.phone || null, + active: v.active ?? true, + dmsid: v.dmsid || null + })) + } + }; + const newShopId = await insertBodyshop(shopInput); + + // Create user + association + const userRecord = await createFirebaseUser(p.userEmail); + const resetLink = await generateResetLink(p.userEmail); + const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId); + + return res.status(200).json({ + shop: { id: newShopId, shopname: p.shopname }, + user: { + id: createdUser.id, + email: createdUser.email, + resetLink + } + }); + } catch (err) { + logger.log("admin-create-shop-user-error", "error", p.userEmail, null, { + message: err.message, + detail: err.detail || err + }); + + // Cleanup on failure + if (err.userRecord) { + await deleteFirebaseUser(err.userRecord.uid).catch(() => {}); + } + if (err.newShopId) { + await deleteVendorsByShop(err.newShopId).catch(() => {}); + await deleteBodyshop(err.newShopId).catch(() => {}); + } + + return res.status(err.status || 500).json({ error: err.message || "Internal server error" }); + } +}; + +module.exports = partsManagementProvisioning; diff --git a/server/integrations/partsManagement/swagger.yaml b/server/integrations/partsManagement/swagger.yaml new file mode 100644 index 000000000..40e28e9b5 --- /dev/null +++ b/server/integrations/partsManagement/swagger.yaml @@ -0,0 +1,160 @@ +openapi: 3.0.3 +info: + title: Parts Management Provisioning API + description: API endpoint to provision a new shop and user in the Parts Management system. + version: 1.0.0 + +paths: + /parts-management/provision: + post: + summary: Provision a new parts management shop and user + operationId: partsManagementProvisioning + tags: + - Parts Management + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - external_shop_id + - shopname + - address1 + - city + - state + - zip_post + - country + - email + - phone + - userEmail + properties: + external_shop_id: + type: string + description: External shop ID (must be unique) + shopname: + type: string + address1: + type: string + address2: + type: string + nullable: true + city: + type: string + state: + type: string + zip_post: + type: string + country: + type: string + email: + type: string + phone: + type: string + userEmail: + type: string + format: email + logoUrl: + type: string + format: uri + nullable: true + timezone: + type: string + nullable: true + vendors: + type: array + items: + type: object + properties: + name: + type: string + street1: + type: string + nullable: true + street2: + type: string + nullable: true + city: + type: string + nullable: true + state: + type: string + nullable: true + zip: + type: string + nullable: true + country: + type: string + nullable: true + email: + type: string + format: email + nullable: true + discount: + type: number + nullable: true + due_date: + type: string + format: date + nullable: true + cost_center: + type: string + nullable: true + favorite: + type: array + items: + type: string + nullable: true + phone: + type: string + nullable: true + active: + type: boolean + nullable: true + dmsid: + type: string + nullable: true + responses: + '200': + description: Shop and user successfully created + content: + application/json: + schema: + type: object + properties: + shop: + type: object + properties: + id: + type: string + format: uuid + shopname: + type: string + user: + type: object + properties: + id: + type: string + email: + type: string + resetLink: + type: string + format: uri + '400': + description: Bad request (missing or invalid fields) + content: + application/json: + schema: + type: object + properties: + error: + type: string + '500': + description: Internal server error + content: + application/json: + schema: + type: object + properties: + error: + type: string diff --git a/server/middleware/partsManagementIntegrationMiddleware.js b/server/middleware/partsManagementIntegrationMiddleware.js new file mode 100644 index 000000000..b564543a4 --- /dev/null +++ b/server/middleware/partsManagementIntegrationMiddleware.js @@ -0,0 +1,23 @@ +/** + * Middleware to check if the request is authorized for Parts Management Integration. + * @param req + * @param res + * @param next + * @returns {*} + */ +const partsManagementIntegrationMiddleware = (req, res, next) => { + const secret = process.env.PARTS_MANAGEMENT_INTEGRATION_SECRET; + if (typeof secret !== "string" || secret.length === 0) { + return res.status(500).send("Server misconfiguration"); + } + + const headerValue = req.headers["parts-management-integration-secret"]; + if (typeof headerValue !== "string" || headerValue.trim() !== secret) { + return res.status(401).send("Unauthorized"); + } + + req.isPartsManagementIntegrationAuthorized = true; + next(); +}; + +module.exports = partsManagementIntegrationMiddleware; diff --git a/server/middleware/vsstaIntegrationMiddleware.js b/server/middleware/vsstaIntegrationMiddleware.js index 44a0bb45d..081c41179 100644 --- a/server/middleware/vsstaIntegrationMiddleware.js +++ b/server/middleware/vsstaIntegrationMiddleware.js @@ -1,16 +1,19 @@ /** * VSSTA Integration Middleware - * @param req - * @param res - * @param next - * @returns {*} + * Fails closed if the env var is missing or empty, and strictly compares header. */ const vsstaIntegrationMiddleware = (req, res, next) => { - if (req?.headers?.["vssta-integration-secret"] !== process.env?.VSSTA_INTEGRATION_SECRET) { + const secret = process.env.VSSTA_INTEGRATION_SECRET; + if (typeof secret !== "string" || secret.length === 0) { + return res.status(500).send("Server misconfiguration"); + } + + const headerValue = req.headers["vssta-integration-secret"]; + if (typeof headerValue !== "string" || headerValue.trim() !== secret) { return res.status(401).send("Unauthorized"); } - req.isIntegrationAuthorized = true; + req.isVsstaIntegrationAuthorized = true; next(); }; diff --git a/server/routes/intergrationRoutes.js b/server/routes/intergrationRoutes.js index 841805675..46826d28e 100644 --- a/server/routes/intergrationRoutes.js +++ b/server/routes/intergrationRoutes.js @@ -1,8 +1,27 @@ const express = require("express"); -const vsstaIntegration = require("../integrations/VSSTA/vsstaIntegrationRoute"); -const vsstaMiddleware = require("../middleware/vsstaIntegrationMiddleware"); const router = express.Router(); -router.post("/vssta", vsstaMiddleware, vsstaIntegration); +// Pull secrets from env +const { VSSTA_INTEGRATION_SECRET, PARTS_MANAGEMENT_INTEGRATION_SECRET } = process.env; + +// Only load VSSTA routes if the secret is set +if (typeof VSSTA_INTEGRATION_SECRET === "string" && VSSTA_INTEGRATION_SECRET.length > 0) { + const vsstaIntegration = require("../integrations/VSSTA/vsstaIntegrationRoute"); + const vsstaMiddleware = require("../middleware/vsstaIntegrationMiddleware"); + + router.post("/vssta", vsstaMiddleware, vsstaIntegration); +} else { + console.warn("VSSTA_INTEGRATION_SECRET is not set — skipping /vssta integration route"); +} + +// Only load Parts Management routes if that secret is set +if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_INTEGRATION_SECRET.length > 0) { + const partsManagementProvisioning = require("../integrations/partsManagement/partsManagementProvisioning"); + const partsManagementIntegrationMiddleware = require("../middleware/partsManagementIntegrationMiddleware"); + + router.post("/parts-management/provision", partsManagementIntegrationMiddleware, partsManagementProvisioning); +} else { + console.warn("PARTS_MANAGEMENT_INTEGRATION_SECRET is not set — skipping /parts-management/provision route"); +} module.exports = router;