NetSuite Integration
Syncs Volare business records (clients, suppliers, bookings, payments) to NetSuite ERP via the REST API, using queue-based jobs with automatic retry and a polymorphic sync tracking system.
When to Use
Section titled “When to Use”- Sync client, supplier, booking, and payment data to NetSuite for accounting
- Monitor sync status and retry failures from the Filament admin panel
- Reconcile local records against NetSuite to detect drift
- Create service items in NetSuite for booking upsell line items
Architecture
Section titled “Architecture”Model (with HasNetsuiteSync trait) -> Sync Job dispatched to `netsuite-sync` queue -> AbstractNetsuiteSyncJob.handle() -> NetsuiteClient (REST requests with retry + 401 re-auth) -> NetsuiteAuthenticationService (OAuth 2.0 JWT assertion, cached token) -> NetsuiteSync record updated (Pending -> Syncing -> Synced/Failed)Key components:
NetsuiteAuthenticationService— OAuth 2.0 client_credentials flow with EC256 JWT assertion. Tokens are cached with a 5-minute buffer before expiry.NetsuiteClient— REST client with configurable timeout, retry (with backoff), automatic 401 re-authentication, typed error handling (429 rate limit, 400/422 validation, 5xx server errors), and structured logging with credential sanitization.NetsuiteSyncRegistry— Maps model types to their sync job class and NetSuite REST endpoint. Used by Artisan commands to dispatch jobs generically.NetsuiteSyncmodel — Polymorphic sync tracking. Storesnetsuite_id,external_id,status,payload,attempts, anderror.HasNetsuiteSynctrait — Applied toBooking,Client,Payment,Supplier. ProvidesnetsuiteSync()relation,getOrCreateNetsuiteSync(), andneedsNetsuiteSync().
Source: backend/app/Services/Netsuite/
Configuration
Section titled “Configuration”Required environment variables
Section titled “Required environment variables”| Variable | Description |
|---|---|
NETSUITE_ENVIRONMENT | sandbox or production (default: sandbox) |
NETSUITE_ACCOUNT_ID | NetSuite account ID (underscores replaced with hyphens for URLs) |
NETSUITE_CLIENT_ID | OAuth 2.0 client ID from NetSuite Integration record |
NETSUITE_CLIENT_SECRET | OAuth 2.0 client secret |
NETSUITE_CERTIFICATE_ID | Certificate ID for JWT signing |
NETSUITE_PRIVATE_KEY_PATH | Path to EC256 private key (default: storage/netsuite/private.pem) |
Optional environment variables
Section titled “Optional environment variables”| Variable | Default | Description |
|---|---|---|
NETSUITE_REST_URL | Auto-derived from account ID | Override REST API base URL |
NETSUITE_TOKEN_URL | Auto-derived from account ID | Override token endpoint |
NETSUITE_SANDBOX_TIMEOUT | 30 | Request timeout (seconds) |
NETSUITE_SANDBOX_RETRY_ATTEMPTS | 3 | Max retry attempts |
NETSUITE_SANDBOX_RETRY_DELAY | 500 | Retry delay (ms) |
NETSUITE_LOGGING_ENABLED | true | Enable API request logging |
NETSUITE_LOG_CHANNEL | single | Log channel |
NETSUITE_QUEUE | netsuite-sync | Queue name for sync jobs |
NETSUITE_SUBSIDIARY_ID | 1 | NetSuite subsidiary internal ID |
NETSUITE_ITEM_FLIGHT | — | Service item ID for flight line items |
NETSUITE_ITEM_HOTEL | — | Service item ID for hotel line items |
NETSUITE_ITEM_ACTIVITY | — | Service item ID for activity line items |
NETSUITE_ITEM_TRANSFER | — | Service item ID for transfer line items |
NETSUITE_ITEM_INSURANCE | — | Service item ID for insurance line items |
NETSUITE_ITEM_FLIGHT_UPGRADE | — | Service item ID for flight upgrade line items |
Config file: backend/config/netsuite.php
Record Mappings
Section titled “Record Mappings”| Volare Model | NetSuite Record | REST Endpoint | Sync Job |
|---|---|---|---|
Client | Customer | /record/v1/customer | SyncClientToNetsuiteJob |
Supplier | Vendor | /record/v1/vendor | SyncSupplierToNetsuiteJob |
Booking | Invoice | /record/v1/invoice | SyncBookingToNetsuiteJob |
Payment | CustomerPayment | /record/v1/customerPayment | SyncPaymentToNetsuiteJob |
| Booking upsells | VendorBill | /record/v1/vendorBill | SyncVendorBillToNetsuiteJob |
The registry (NetsuiteSyncRegistry) covers the first four. SyncVendorBillToNetsuiteJob is standalone — it groups a booking’s upsells by supplier and creates one Vendor Bill per supplier, using contract cost prices when available.
Source: backend/app/Services/Netsuite/NetsuiteSyncRegistry.php
Sync Status Lifecycle
Section titled “Sync Status Lifecycle”Tracked by NetsuiteSyncStatus enum:
- Pending — Awaiting sync (initial state, or reset for retry)
- Syncing — Job is currently executing
- Synced — Successfully synced,
netsuite_idis populated - Failed — Sync failed,
errorfield has details - Skipped — Intentionally excluded from sync
Source: backend/app/Enums/NetsuiteSyncStatus.php
Business Rules
Section titled “Business Rules”- Dependency ordering: Client must be synced before its Booking or Payment. Supplier must be synced before its VendorBill. Jobs throw
RuntimeExceptionif the dependency is missing. - Idempotency: Each model gets a deterministic
externalId(volare_{table}_{id}). POST creates new records, PUT updates existing ones (whennetsuite_idis already set). - Retry: Jobs retry up to 3 times with backoff (30s, 60s, 180s). After all retries, the sync record is marked Failed. Non-retryable errors (e.g., validation) fail immediately without retrying.
- Rate limiting: 429 responses release the job back to the queue with the
Retry-Afterdelay (default 60s). The sync record stays Pending during the wait. - Currency mapping: ISO codes mapped to NetSuite internal IDs in
config/netsuite.php(currencieskey). Currently: EUR, USD, CAD, GBP. - Invoice line items: Built from
BookingUpsellrecords, mapped to NetSuite service items by upsell type. Falls back to a single “Flight” line if no upsells exist. - Vendor Bill cost: Looks up
SupplierContractServiceprice from the active contract valid at booking date.
Artisan Commands
Section titled “Artisan Commands”netsuite:sync-all
Section titled “netsuite:sync-all”Dispatch sync jobs for all unsynced records.
vendor/bin/sail artisan netsuite:sync-allvendor/bin/sail artisan netsuite:sync-all --model=client --limit=100vendor/bin/sail artisan netsuite:sync-all --force # re-sync already synced recordsnetsuite:retry-failed
Section titled “netsuite:retry-failed”Re-dispatch jobs for failed or pending sync records.
vendor/bin/sail artisan netsuite:retry-failedvendor/bin/sail artisan netsuite:retry-failed --status=pending --model=booking --limit=50vendor/bin/sail artisan netsuite:retry-failed --status=allnetsuite:reconcile
Section titled “netsuite:reconcile”Compare local sync records against NetSuite. Reports verified, missing, and errored records.
vendor/bin/sail artisan netsuite:reconcilevendor/bin/sail artisan netsuite:reconcile --fix # reset stuck/failed recordsThe --fix flag resets “syncing” records older than 1 hour to Failed, then resets all Failed records to Pending.
netsuite:sync-service-items
Section titled “netsuite:sync-service-items”Create service items in NetSuite for each upsell type (flight, hotel, activity, transfer, insurance, flight_upgrade). Outputs the NETSUITE_ITEM_* env vars to add to .env.
vendor/bin/sail artisan netsuite:sync-service-itemsSource: backend/app/Console/Commands/
Filament Admin
Section titled “Filament Admin”NetsuiteSyncMonitor page
Section titled “NetsuiteSyncMonitor page”Admin-only page under System > NetSuite Sync. Shows a table of all NetsuiteSync records with:
- Filters by status and model type
- Retry action on failed records (resets to Pending)
- Auto-polls every 30 seconds
NetsuiteSyncStatsWidget
Section titled “NetsuiteSyncStatsWidget”Stats overview (total, synced, failed, pending) displayed as header widget on the monitor page.
Source: backend/app/Filament/Pages/NetsuiteSyncMonitor.php, backend/app/Filament/Widgets/NetsuiteSyncStatsWidget.php
Adding a New Syncable Model
Section titled “Adding a New Syncable Model”- Add the trait to your model:
use HasNetsuiteSync; - Create a sync job extending
AbstractNetsuiteSyncJob. ImplementbuildPayload(),getEndpoint(), andgetRecordType(). - Register in
NetsuiteSyncRegistry::MODELSwith the model class, job class, and NetSuite endpoint. - Run
netsuite:sync-all --model=yourmodelto dispatch.
Source: backend/app/Jobs/Netsuite/AbstractNetsuiteSyncJob.php for the base job contract.
Exception Hierarchy
Section titled “Exception Hierarchy”All exceptions extend NetsuiteException and implement isRetryable():
| Exception | HTTP Status | Retryable | Description |
|---|---|---|---|
NetsuiteException | any | No | Base exception for all NetSuite errors |
NetsuiteAuthenticationException | 401 | Yes | OAuth token expired or credentials invalid |
NetsuiteTimeoutException | — | Yes | Connection timeout |
NetsuiteRateLimitException | 429 | Yes | Rate limit exceeded, carries retryAfter delay |
NetsuiteValidationException | 400/422 | No | Payload validation failed, carries validationErrors |
Source: backend/app/Exceptions/Services/Netsuite/
Troubleshooting
Section titled “Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
InvalidArgumentException: NetSuite account ID is not configured | Missing env vars | Set NETSUITE_ACCOUNT_ID, NETSUITE_CLIENT_ID, NETSUITE_CERTIFICATE_ID |
NetsuiteAuthenticationException: Private key file not found | Key not deployed | Place EC256 private key at NETSUITE_PRIVATE_KEY_PATH (default: storage/netsuite/private.pem) |
NetsuiteValidationException on sync | Invalid payload data | Check the error field on the sync record for NetSuite validation details |
NetsuiteRateLimitException | Too many API calls | Job auto-retries after delay; reduce batch size in netsuite:sync-all --limit |
RuntimeException: Client must be synced before booking | Dependency not synced | Sync clients first: netsuite:sync-all --model=client, then bookings |
| Sync stuck in “Syncing” state | Job crashed mid-execution | Run netsuite:reconcile --fix to reset stuck records |
| 401 after token refresh | Certificate/credentials rotated in NetSuite | Update NETSUITE_CLIENT_ID, NETSUITE_CLIENT_SECRET, NETSUITE_CERTIFICATE_ID in .env |
Related
Section titled “Related”- Config:
backend/config/netsuite.php - Services:
backend/app/Services/Netsuite/ - Jobs:
backend/app/Jobs/Netsuite/ - Model:
backend/app/Models/NetsuiteSync.php - Trait:
backend/app/Traits/HasNetsuiteSync.php - Enum:
backend/app/Enums/NetsuiteSyncStatus.php - Commands:
backend/app/Console/Commands/Netsuite*.php - Exceptions:
backend/app/Exceptions/Services/Netsuite/