{"openapi":"3.0.0","info":{"title":"Händigo API","version":"1.0.0","description":"Backend API for Händigo — a Swedish home-services marketplace. Vi fixar. Du njuter."},"servers":[{"url":"http://localhost:4000","description":"Local development server"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Role":{"type":"string","enum":["admin","employee","customer","partner"]},"BookingStatus":{"type":"string","enum":["confirmed","assigned","enroute","in_progress","completed"]},"InvoiceStatus":{"type":"string","enum":["unpaid","paid"]},"MessageFrom":{"type":"string","enum":["customer","pro"]},"PhotoKind":{"type":"string","enum":["before","after"]},"Frequency":{"type":"string","enum":["once","weekly","biweekly","monthly"]},"ServiceCategory":{"type":"string","enum":["hem","fastighet","foretag"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"code":{"type":"string"},"details":{"type":"object"}}}}},"User":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"email":{"type":"string","format":"email"},"role":{"$ref":"#/components/schemas/Role"},"roleId":{"type":"string","format":"uuid"},"phone":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"Service":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"category":{"$ref":"#/components/schemas/ServiceCategory"},"icon":{"type":"string"},"tagline":{"type":"string"},"description":{"type":"string"},"priceFrom":{"type":"integer"},"unit":{"type":"string"},"duration":{"type":"string"},"rating":{"type":"number","nullable":true},"reviewCount":{"type":"integer"},"popular":{"type":"boolean"},"rutAvdrag":{"type":"boolean"},"includes":{"type":"array","items":{"type":"string"}},"img":{"type":"string"}}},"Professional":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"userId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string"},"email":{"type":"string","nullable":true},"title":{"type":"string"},"city":{"type":"string","nullable":true},"color":{"type":"string","nullable":true},"rating":{"type":"number","nullable":true},"reviews":{"type":"integer"},"jobs":{"type":"integer"},"years":{"type":"integer"},"verified":{"type":"boolean"},"bio":{"type":"string","nullable":true},"skills":{"type":"array","items":{"type":"string"}},"langs":{"type":"array","items":{"type":"string"}},"serviceIds":{"type":"array","items":{"type":"string","format":"uuid"}},"createdAt":{"type":"string","format":"date-time"}}},"Booking":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"customerId":{"type":"string","format":"uuid"},"serviceId":{"type":"string","format":"uuid"},"proId":{"type":"string","format":"uuid","nullable":true},"date":{"type":"string","format":"date"},"time":{"type":"string"},"address":{"type":"string"},"frequency":{"$ref":"#/components/schemas/Frequency"},"hours":{"type":"integer"},"notes":{"type":"string"},"price":{"type":"integer"},"status":{"$ref":"#/components/schemas/BookingStatus"},"createdAt":{"type":"string","format":"date-time"}}},"Message":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bookingId":{"type":"string","format":"uuid"},"sender":{"$ref":"#/components/schemas/MessageFrom"},"text":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"Invoice":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bookingId":{"type":"string","format":"uuid"},"number":{"type":"string"},"amount":{"type":"integer"},"rutRot":{"type":"integer"},"vat":{"type":"integer"},"total":{"type":"integer"},"status":{"$ref":"#/components/schemas/InvoiceStatus"},"paymentType":{"type":"string","enum":["BG","PG"],"nullable":true},"account":{"type":"string","nullable":true},"ocr":{"type":"string","nullable":true},"issuedAt":{"type":"string","format":"date-time"},"dueAt":{"type":"string","format":"date-time"},"paidAt":{"type":"string","format":"date-time","nullable":true},"method":{"type":"string","nullable":true}}},"Review":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"bookingId":{"type":"string","format":"uuid"},"customerId":{"type":"string","format":"uuid"},"proId":{"type":"string","format":"uuid"},"serviceId":{"type":"string","format":"uuid"},"rating":{"type":"integer","minimum":1,"maximum":5},"text":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}}}},"security":[],"paths":{"/api/services":{"get":{"summary":"List all services","tags":["Services"],"responses":{"200":{"description":"List of services","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}}}}}},"post":{"summary":"Create a service (admin)","tags":["Services"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["slug","name","category","icon","priceFrom"],"properties":{"slug":{"type":"string"},"name":{"type":"string"},"category":{"type":"string","enum":["hem","fastighet","foretag"]},"icon":{"type":"string"},"tagline":{"type":"string"},"description":{"type":"string"},"priceFrom":{"type":"integer"},"unit":{"type":"string"},"duration":{"type":"string"},"rating":{"type":"number"},"reviewCount":{"type":"integer"},"popular":{"type":"boolean"},"rutAvdrag":{"type":"boolean"},"includes":{"type":"array","items":{"type":"string"}},"img":{"type":"string"}}}}}},"responses":{"201":{"description":"Service created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Service"}}}}}}},"/api/services/{id}":{"get":{"summary":"Get a single service by id","tags":["Services"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Service found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Service"}}}},"404":{"description":"Service not found"}}},"put":{"summary":"Update a service (admin)","tags":["Services"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"Service updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Service"}}}}}},"delete":{"summary":"Delete a service (admin)","tags":["Services"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Service deleted"},"404":{"description":"Service not found"}}}},"/api/reviews":{"get":{"summary":"List reviews (optionally filtered by service or pro)","tags":["Reviews"],"parameters":[{"in":"query","name":"serviceId","schema":{"type":"string","format":"uuid"}},{"in":"query","name":"proId","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List of reviews","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Review"}}}}}}}},"/api/bookings/{id}/review":{"post":{"summary":"Leave a review for a completed booking (customer)","tags":["Reviews"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rating"],"properties":{"rating":{"type":"integer","minimum":1,"maximum":5},"text":{"type":"string"}}}}}},"responses":{"201":{"description":"Review created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Review"}}}}}}},"/api/professionals":{"get":{"summary":"List all professionals","tags":["Professionals"],"responses":{"200":{"description":"List of professionals","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Professional"}}}}}}},"post":{"summary":"Create a professional AND linked employee account (admin)","tags":["Professionals"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email","title"],"properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"title":{"type":"string"},"city":{"type":"string"},"color":{"type":"string"},"rating":{"type":"number"},"reviews":{"type":"integer"},"jobs":{"type":"integer"},"years":{"type":"integer"},"verified":{"type":"boolean"},"bio":{"type":"string"},"skills":{"type":"array","items":{"type":"string"}},"langs":{"type":"array","items":{"type":"string"}},"serviceIds":{"type":"array","items":{"type":"string"}},"password":{"type":"string","minLength":6}}}}}},"responses":{"201":{"description":"Professional created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Professional"}}}},"409":{"description":"An account with this email already exists"}}}},"/api/professionals/me":{"get":{"summary":"Get the current employee's own professional profile","tags":["Professionals"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The current user's professional profile","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Professional"}}}},"404":{"description":"No professional profile linked to this account"}}},"put":{"summary":"Update own professional profile (employee)","description":"Employees update their own profile: name, title, city, years, bio, skills, langs, serviceIds. Name also syncs to the linked user account.\n","tags":["Professionals"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"title":{"type":"string"},"city":{"type":"string","nullable":true},"years":{"type":"integer"},"bio":{"type":"string","nullable":true},"skills":{"type":"array","items":{"type":"string"}},"langs":{"type":"array","items":{"type":"string"}},"serviceIds":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}},"responses":{"200":{"description":"Profile updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Professional"}}}},"404":{"description":"No professional profile linked to this account"}}}},"/api/professionals/{id}":{"get":{"summary":"Get a single professional by id","tags":["Professionals"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Professional found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Professional"}}}},"404":{"description":"Professional not found"}}},"put":{"summary":"Update a professional's admin-managed fields (admin)","description":"Admin may set title, serviceIds, verified and name. Presentation fields are edited by the employee via PUT /api/professionals/me.\n","tags":["Professionals"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"title":{"type":"string"},"color":{"type":"string"},"verified":{"type":"boolean"},"serviceIds":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Professional updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Professional"}}}},"404":{"description":"Professional not found"}}},"delete":{"summary":"Deactivate/delete a professional (admin)","tags":["Professionals"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Professional deleted"},"404":{"description":"Professional not found"}}}},"/api/professionals/{id}/reviews":{"get":{"summary":"List reviews for a professional","tags":["Professionals"],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List of reviews","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Review"}}}}}}}},"/api/invoices":{"get":{"summary":"List invoices (customers see own, staff see all)","tags":["Invoices"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"status","schema":{"$ref":"#/components/schemas/InvoiceStatus"}}],"responses":{"200":{"description":"List of invoices","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}}}}}}}},"/api/invoices/{id}":{"get":{"summary":"Get a single invoice","tags":["Invoices"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Invoice found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}},"404":{"description":"Invoice not found"}}}},"/api/invoices/{id}/pay":{"post":{"summary":"Mark an invoice as paid (customer)","tags":["Invoices"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["method"],"properties":{"method":{"type":"string"}}}}}},"responses":{"200":{"description":"Invoice marked paid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invoice"}}}}}}},"/api/invoices/{id}/pdf":{"get":{"summary":"Download the invoice as a branded PDF (with bank + Swish QR codes)","tags":["Invoices"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Invoice PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Invoice not found"}}}},"/api/invoices/{id}/send":{"post":{"summary":"Email the invoice PDF to the customer (staff only)","tags":["Invoices"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Email sent"},"400":{"description":"Customer has no email address"}}}},"/api/bookings":{"get":{"summary":"List bookings (customers see own, staff see all)","tags":["Bookings"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"status","schema":{"$ref":"#/components/schemas/BookingStatus"}}],"responses":{"200":{"description":"List of bookings","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Booking"}}}}}}},"post":{"summary":"Create a booking (customer)","tags":["Bookings"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["serviceId","date","time","address","frequency","hours","price"],"properties":{"serviceId":{"type":"string","format":"uuid"},"date":{"type":"string","format":"date"},"time":{"type":"string"},"address":{"type":"string"},"frequency":{"$ref":"#/components/schemas/Frequency"},"hours":{"type":"integer"},"notes":{"type":"string"},"price":{"type":"integer"},"promoCode":{"type":"string","description":"Discount code to apply (single-use codes are redeemed atomically)"}}}}}},"responses":{"201":{"description":"Booking created with status 'confirmed'","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}}}}},"/api/bookings/{id}":{"get":{"summary":"Get a single booking","tags":["Bookings"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Booking found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}},"404":{"description":"Booking not found"}}},"put":{"summary":"Update booking details (staff) — e.g. actual hours worked","tags":["Bookings"],"security":[{"bearerAuth":[]}]}},"/api/bookings/{id}/assign":{"post":{"summary":"Assign a professional to a booking (staff)","tags":["Bookings"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["proId"],"properties":{"proId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Booking assigned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}}}}},"/api/bookings/{id}/advance":{"post":{"summary":"Advance booking status one step forward (staff)","tags":["Bookings"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Booking advanced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Booking"}}}}}}},"/api/bookings/{id}/decline":{"post":{"summary":"Decline a customer booking with a required reason (staff)","tags":["Bookings"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Booking declined"},"400":{"description":"Reason is required"}}}},"/api/bookings/{id}/invoice":{"post":{"summary":"Create (or return the existing) invoice for a booking (staff)","tags":["Bookings"],"security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Invoice created / returned"}}}},"/api/bookings/{id}/messages":{"get":{"summary":"List messages for a booking","tags":["Bookings"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"List of messages","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Message"}}}}}}},"post":{"summary":"Send a message on a booking","tags":["Bookings"],"security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sender","text"],"properties":{"sender":{"$ref":"#/components/schemas/MessageFrom"},"text":{"type":"string"}}}}}},"responses":{"201":{"description":"Message created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Message"}}}}}}},"/api/auth/register":{"post":{"summary":"Register a new customer account","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email","password"],"properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":6},"welcomeOffer":{"type":"boolean","description":"Issue a single-use 15% welcome discount code (valid 1 year) and email it"}}}}}},"responses":{"201":{"description":"Account created","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"user":{"$ref":"#/components/schemas/User"},"welcomeCode":{"type":"string","description":"Issued welcome discount code (when welcomeOffer is true)"}}}}}},"409":{"description":"Email already registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/auth/login":{"post":{"summary":"Log in and receive a JWT","tags":["Auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}}}}}},"responses":{"200":{"description":"Successful login","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}}}}}},"401":{"description":"Invalid credentials"}}}},"/api/auth/me":{"get":{"summary":"Get the currently authenticated user","tags":["Auth"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Current user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Unauthenticated"}}},"put":{"summary":"Update own account info (name, email, phone, address, city)","tags":["Auth"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","format":"email"},"phone":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Updated user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"409":{"description":"Email already taken"}}}},"/api/auth/permissions":{"get":{"summary":"Get the current user's permission list (for UI gating)","tags":["Auth"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Current user's permissions","content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string"},"permissions":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/auth/password":{"put":{"summary":"Change own password (verifies current password)","tags":["Auth"],"security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["currentPassword","newPassword"],"properties":{"currentPassword":{"type":"string"},"newPassword":{"type":"string","minLength":6}}}}}},"responses":{"200":{"description":"Password changed","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}}}}}},"400":{"description":"Wrong current password"}}}},"/api/admin/stats":{"get":{"summary":"Dashboard statistics (admin/employee)","tags":["Admin"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Aggregated statistics","content":{"application/json":{"schema":{"type":"object","properties":{"newBookings":{"type":"integer"},"active":{"type":"integer"},"completedToday":{"type":"integer"},"revenue":{"type":"integer"},"customerCount":{"type":"integer"},"openInvoicesTotal":{"type":"integer"}}}}}}}}},"/api/admin/customers":{"get":{"summary":"List all customers (admin/employee)","tags":["Admin"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of customers","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}}},"/api/admin/pros":{"get":{"summary":"List all professionals (admin/employee)","tags":["Admin"],"security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of professionals","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Professional"}}}}}}}},"/health":{"get":{"summary":"Health check","tags":["Health"],"responses":{"200":{"description":"Service is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"uptime":{"type":"number"}}}}}}}}}},"tags":[]}