Conversation
yushinj
commented
Apr 9, 2026
- add Bill.co vendor payload/address mapping fix
- fallback to create when existing bill_vendor_id is invalid
- check vendor network status after sync and trigger invite/reminder when not connected
- improve sync-all batching/aggregation efficiency
- add/expand vendor sync tests
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Implements automated Bill.com vendor synchronization for technicians, including payload mapping fixes, “update-then-create” fallback behavior, post-sync network connection checks with invite/reminder flows, and supporting API routes/tests.
Changes:
- Added Bill.com vendor sync library + API routes (
/api/bill/vendors/sync,/sync-all,/debug) and expanded technician address capture to support vendor payload requirements. - Implemented network-status check after vendor sync to trigger Bill.com invite and a Resend reminder email when not connected.
- Added/updated Vitest coverage for vendor payload validation and vendor sync route authorization/error mapping.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| my-app/src/test/vendor.test.ts | Adds unit tests for buildVendorPayload validation/mapping behavior. |
| my-app/src/test/vendor-sync-route.test.ts | Adds API route tests for authZ and error/status mapping for vendor sync. |
| my-app/src/test/address-management.test.tsx | Updates router mocks to align with navigation usage changes. |
| my-app/src/app/lib/email.ts | Adds a new vendor connection invite/reminder email template and sender. |
| my-app/src/app/lib/bill/vendor.ts | New vendor sync implementation: payload building, upsert logic, status persistence, network-status checks, invite/reminder trigger, and debug helpers. |
| my-app/src/app/lib/bill/types.ts | Introduces types for vendor debugging + future AP bill automation. |
| my-app/src/app/lib/bill/client.ts | New Bill.com client for vendor endpoints (create/update/get vendor, network status, invite). |
| my-app/src/app/components/EditProfile.tsx | Collects technician address fields and triggers vendor sync after profile save. |
| my-app/src/app/components/CompleteTechInfo.tsx | Collects technician address fields and best-effort triggers vendor sync after onboarding. |
| my-app/src/app/components/CompleteProfile.tsx | Hardens session loading and token handling in profile completion. |
| my-app/src/app/components/AuthStatus.tsx | Improves resilience around session/role lookups and sign-out cleanup. |
| my-app/src/app/components/AuthGate.tsx | Adds error handling around session retrieval before gating. |
| my-app/src/app/api/create-profile/route.ts | Persists technician address fields with legacy-schema fallback behavior. |
| my-app/src/app/api/bill/vendors/sync/route.ts | Adds vendor sync endpoint with admin/self-tech authorization. |
| my-app/src/app/api/bill/vendors/sync-all/route.ts | Adds admin-only batched “sync all pending vendors” endpoint. |
| my-app/src/app/api/bill/vendors/debug/route.ts | Adds admin-only debug endpoint that reports sync readiness without Bill.com calls. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Prepare the request body based on role | ||
| type RequestBody = { | ||
| role: "lab" | "technician" | null | ||
| full_name: string | ||
| phone: string | ||
| lab?: { | ||
| name: string | ||
| } | ||
| tech?: { | ||
| experience: string | ||
| bio: string | ||
| company: string | null | ||
| line1: string | null | ||
| line2: string | null | ||
| city: string | null | ||
| state: string | null | ||
| zipcode: string | null | ||
| } | ||
| } | ||
|
|
||
| const requestBody: RequestBody = { | ||
| role: profile?.role ?? null, | ||
| role: profile?.role === "lab" || profile?.role === "technician" ? profile.role : null, | ||
| full_name: fullName, | ||
| phone: normalizedPhone | ||
| } |
There was a problem hiding this comment.
requestBody.role is forced to null for any role other than lab/technician. Since this component can load an admin profile (type now includes admin), submitting the form would clear the user’s role in profiles and effectively remove admin privileges. Preserve admin (or omit role from the request when editing admin profiles) so admin users don’t get downgraded.
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Already applied in commit 4c509e6. RequestBody.role now includes "admin" and the role assignment preserves it: profile?.role === "lab" || profile?.role === "technician" || profile?.role === "admin" ? profile.role : null.
There was a problem hiding this comment.
Already in place from commit 4c509e6. RequestBody.role includes "admin" and the assignment on line 247 preserves it: profile?.role === "lab" || profile?.role === "technician" || profile?.role === "admin" ? profile.role : null.
| const network = await billClient.getVendorNetworkStatus(billResult.id); | ||
| log(`Vendor ${billResult.id} network status: ${network.status}${network.rawStatus ? ` (${network.rawStatus})` : ""}`); | ||
|
|
||
| if (network.status !== "connected") { | ||
| try { | ||
| await billClient.sendVendorConnectionInvite(billResult.id, profile.email || undefined); | ||
| log(`Sent Bill.com connection invite for vendor ${billResult.id}`); | ||
| } catch (inviteErr) { | ||
| const message = inviteErr instanceof Error ? inviteErr.message : String(inviteErr); | ||
| logError(`Bill.com invite endpoint failed for vendor ${billResult.id}:`, message); | ||
| } | ||
|
|
||
| if (profile.email) { | ||
| const reminder = await sendVendorConnectionInviteEmail({ | ||
| recipientEmail: profile.email, | ||
| recipientName: profile.full_name, | ||
| vendorName: payloadResult.payload.name, | ||
| vendorId: billResult.id, | ||
| }); | ||
|
|
||
| if (reminder.success) { | ||
| log(`Sent vendor connection reminder email to ${profile.email}`); | ||
| } else { | ||
| logError(`Failed to send vendor reminder email to ${profile.email}`); | ||
| } | ||
| } else { | ||
| logError(`Cannot send vendor connection reminder email: profile email missing for ${technicianId}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
The invite/reminder logic triggers for any status other than connected, including unknown (when the API response doesn’t contain a parseable network status). That can generate unnecessary invites/emails on transient parsing/API issues. Only send invites/reminders when the normalized status is explicitly not_connected (and consider skipping on unknown).
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Already applied in commit 4c509e6. Changed network.status !== "connected" to network.status === "not_connected" so invite/reminder emails are only sent for an explicit not_connected state, skipping unknown and other transient statuses.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…role, login URL, vendor invite status, sync-all filter Agent-Logs-Url: https://github.com/gordon-cs/Biotech-Maintenance-Platform/sessions/fcf9c524-e7e6-41e8-aaaa-08c2927f67b7 Co-authored-by: yushinj <149107617+yushinj@users.noreply.github.com>
|
@copilot apply changes based on the comments in this thread |
# Conflicts: # my-app/src/app/api/create-profile/route.ts Co-authored-by: yushinj <149107617+yushinj@users.noreply.github.com>
All six review comments were addressed in commit |