Appearance
LGL Sync Audit (UIEngine v3)
This document is a read-only, code-derived reference of the current Little Green Light (LGL) synchronization system in ui-v3.
1) File Map
Core LGL Sync Orchestration
src/services/lgl-sync.ts- Central orchestration for syncing users, memberships, payments/gifts, enrollments, and family relationships to LGL.src/clients/lgl.ts- Low-level LGL API client with auth, retry/backoff, rate limiting, fund/payment-type/membership-level mapping, and relationship helpers.src/services/sync-queue.ts- Sync queue service for enqueueing and tracking LGL/WordPress/messaging jobs.src/services/sync-queue-processor.ts- Background queue worker that routes LGL jobs tolgl-synchandlers.
Database Layer and Schema
src/db/index.ts- ExportslglSettingsRepo,syncJobRepo,auditLogRepo, and entity repos used by LGL sync flow.src/db/migrations/006_lgl_settings.sql- Creates LGL settings storage used for API/fund/payment/membership/relationship configuration.src/db/migrations/010_lgl_matching_audit.sql- Adds constituent matching audit table for dedupe/linking diagnostics.src/db/migrations/015_add_lgl_membership_id.sql- Adds LGL membership linkage tracking on local membership records.src/db/migrations/049_add_lgl_relationship_columns.sql- Adds LGL relationship linkage fields.src/db/migrations/050_add_lgl_relationship_type_to_invitations.sql- Adds relationship type support in invitation flow.
Triggers, Queue Producers, and Business Flows
src/routes/webhooks.ts- Stripe webhook ingestion and primary source of queued LGL sync jobs.src/routes/checkout.ts- Checkout creation flow that writes payment metadata consumed by webhook/LGL sync and handles zero-dollar orders.src/routes/checkout-scholarship.ts- Scholarship checkout flow, including$0payment records that still queue LGL payment sync.src/services/checkout-handlers.ts- Shared post-purchase handlers that create enrollment/membership outcomes and enqueue related sync jobs.src/services/membership.ts- Membership creation/renewal logic that enqueues membership sync jobs.src/services/enrollment.ts- Enrollment confirmation flow that enqueues enrollment sync jobs.
Admin API Endpoints (LGL + Sync Ops)
src/routes/admin/sync.ts- Admin routes for sync jobs listing, retry, manual LGL sync actions, and LGL connection test.src/routes/admin/settings.ts- Unified settings API including LGL API config updates and cache invalidation.src/routes/admin/lgl.ts- LGL-specific admin configuration and utility endpoints (funds, mapping, relationship types, test scenarios, queue helpers).
Admin UI Surfaces
admin/app/admin/sync/page.tsx- General sync jobs page (cross-target, includes retry action).admin/app/admin/sync/lgl/page.tsx- Legacy/alternate dedicated LGL sync operations page (manual sync, queue, test scenarios).admin/app/admin/system/lgl/page.tsx- Current LGL Sync Hub with users/payments/memberships/relationships/queue tabs.admin/app/admin/settings/lgl/page.tsx- LGL settings UI (environment, API config, payment/membership/relationship mapping, sync toggles).admin/app/components/Sidebar.tsx- Navigation links to LGL Sync Hub and settings.
Additional LGL References
src/config/env.ts- Environment-level LGL variables and scholarship group IDs used by sync services.src/types/*andpackages/shared/src/types/*- Type definitions for sync entities consumed in queue/LGL handlers.
2) What Gets Synced to LGL
User / Constituent Sync
UIEngine Data Sent
From users record:
id(as external id)emailfirst_name,last_namephonebilling_address_1,billing_address_2,billing_city,billing_state,billing_postcode,billing_country- existing linkage:
lgl_constituent_id
LGL Object Mapping
- UIEngine
users-> LGLconstituents - Local
users.lgl_constituent_idstores LGL constituent ID
Payload Fields (Constituent + Contact)
Constituent create/update payload includes:
first_name,last_nameexternal_constituent_id(UIEngineuser.id)- default profile fields (
is_org, contact type, addressee/salutation/sort fields, etc.)
Contact info is synced separately:
- Email -> LGL emails endpoint
- Phone -> LGL phones endpoint
- Address -> LGL addresses endpoint (
street,city,state,postal_code,country)
Conditional Logic and Deduplication
- If local
lgl_constituent_idexists, system re-validates linkage by checking LGL email(s). - If existing LGL constituent email mismatches local user email, linkage is cleared and user is treated as needing new/matching lookup.
- Matching strategy uses name search + client-side email filtering (
searchByNameAndEmail); direct email-only search is intentionally deprecated. - If exact match not found, a new LGL constituent is created.
Handler
src/services/lgl-sync.ts->syncUserToLGL(userId)- Uses
src/clients/lgl.ts->findOrCreateConstituent,updateConstituent,syncConstituentContactInfo
Membership Sync
UIEngine Data Sent
From memberships and linked users:
membership.id,user_id,level,statusstart_date,renewal_datesubsidy_tier(scholarship handling)- product/membership-level context for mapping
LGL Object Mapping
- UIEngine
memberships-> LGLmemberships - Local
memberships.lgl_membership_idstores LGL membership ID
Payload Fields (Membership)
constituent_idmembership_level_id(mapped via product/global mapping)date_start<- UIEnginestart_datefinish_date<- UIEnginerenewal_dateis_current: truenotewith sync context
Conditional Logic
- If user has no LGL constituent,
syncUserToLGLruns first. - Before creating a new LGL membership, existing current LGL memberships for that constituent are deactivated (
finish_dateset to yesterday or start-date-safe fallback,is_current=false). - Scholarship tiers can add user to configured LGL groups:
- full scholarship ->
LGL_GROUP_ID_SCHOLARSHIP_FULL - partial scholarship ->
LGL_GROUP_ID_SCHOLARSHIP_PARTIAL
- full scholarship ->
Handler
src/services/lgl-sync.ts->syncMembershipToLGL(membershipId)
Payment / Gift Sync
UIEngine Data Sent
From payments + optional payment_items + related user/enrollment/product context:
payment.id,user_id,type,status,gatewayamount_cents,created_at,stripe_payment_intent_idlgl_gift_id(idempotency marker)- line items for cart splitting
LGL Object Mapping
- UIEngine
payments-> LGLgifts - Local
payments.lgl_gift_idstores resulting gift linkage (or multiple gifts in cart mode tracked through process; primary linkage remains on payment record)
Exact Gift Payload Fields
Gift payload uses:
constituent_idreceived_datereceived_amount(amount_cents / 100)fund_id(from fund mapping)campaign_name(from fund mapping)payment_type_id(from gateway mapping)external_id(usuallypayment.id, cart may append suffix per grouped subtype)note(includes payment intent/status/date context)
Gift Type Values, Triggers, and Assignment
Gift type routing is driven by payment.type and item/product context in syncPaymentToLGL:
membership- Fund config key:
membership - Calls
createMembershipGift
- Fund config key:
enrollment- Looks up related enrollment/product type
class-> fund keylanguage_classes, callscreateClassGiftevent-> fund keyevents, callscreateEventGift- fallback -> membership fund/key
donation- Fund key
donations, callscreateGift
- Fund key
cart- Splits/group items by product category and creates one gift per group
- Membership-like items (including family add-on handling) route to membership config
- Class items route to language classes config
- Event items route to events config
- Donation items route to donations config
Assignment location: src/services/lgl-sync.ts in syncPaymentToLGL.
Conditional Logic
- Only succeeded payments sync (
status === "succeeded"). - If
lgl_gift_idalready exists, sync is skipped unlessforce=true. - Ensures user has valid LGL constituent; may trigger
syncUserToLGL. - After successful membership-related payment sync, active membership sync is triggered to keep LGL membership current.
Handler
src/services/lgl-sync.ts->syncPaymentToLGL(paymentId, { force? })
Enrollment Sync
UIEngine Data Sent
- Enrollment entity identifiers/status are recorded locally and in audit logs.
- Monetary and donor-facing representation in LGL is primarily via payment->gift sync, not a standalone enrollment object.
LGL Object Mapping
- UIEngine
enrollmentsare represented indirectly as gift records tied to class/event fund/campaign.
Conditional Logic
- Enrollment sync job handler mainly logs successful sync marker.
- Payment sync is the authoritative path for gift creation tied to enrollment transactions.
Handler
src/services/lgl-sync.ts->syncEnrollmentToLGL(enrollmentId)
Event Registration Sync
UIEngine Data Sent
- Event registrations are a subtype of enrollment/product purchase.
- Payment details and product type (
event) determine gift categorization.
LGL Object Mapping
- Event registration revenue -> LGL gifts with event fund/campaign mapping.
Conditional Logic
- Triggered by successful payment pipeline.
- Product typed as
eventroutes to event gift creation (createEventGift).
Handler
- Event path is implemented through
syncPaymentToLGLand enrollment/payment metadata.
Family / Relationship Sync
UIEngine Data Sent
From family member relationship records:
primary_user_id,member_user_id- optional
lgl_relationship_type_id - existing
lgl_relationship_id
LGL Object Mapping
- UIEngine family link -> LGL constituent relationship
- Local
family_members.lgl_relationship_idstores linked relationship record
Payload Fields (Relationship)
constituent_id(primary)related_constituent_id(member)relationship_type_id- sharing flags (address/phone/credit/ack settings) as defined in client helper
Conditional Logic
- If relation already has
lgl_relationship_id, skip. - If either user missing LGL constituent, sync users first.
- Prefer configured/default relationship type ID.
- Legacy fallback path exists for deprecated string-based relationship helper when type ID unavailable.
Handler
src/services/lgl-sync.ts->syncFamilyRelationshipToLGL(primaryUserId, memberUserId)
3) Sync Queue Architecture
Where Jobs Are Created
Primary enqueue sites:
- Stripe webhook handlers in
src/routes/webhooks.ts(payment success and checkout completion paths) - Membership lifecycle in
src/services/membership.ts - Enrollment confirmation in
src/services/enrollment.tsandsrc/services/checkout-handlers.ts - Scholarship checkout paths in
src/routes/checkout-scholarship.ts - Admin manual queue endpoints in
src/routes/admin/sync.ts/src/routes/admin/lgl.ts
Job creation call:
queueSyncJob(target, entityType, entityId)insrc/services/sync-queue.ts
Queue Model / Table
Backed by sync_jobs via syncJobRepo in src/db/index.ts.
Core fields used operationally:
idtarget(lgl,wordpress,messaging)entity_type(e.g.user,payment,membership,enrollment,relationship)entity_idstatus(queued,running,succeeded,failed)attemptslast_errorcreated_at,updated_at
Job Triggers
- Stripe events (
checkout.session.completed,payment_intent.succeeded) create or update domain records, then enqueue LGL jobs. - Membership and enrollment service methods enqueue entity-specific sync jobs as domain state transitions to paid/active.
- Admin UI can manually queue or directly invoke sync handlers.
Background Processor
In src/services/sync-queue-processor.ts:
- Poll interval:
PROCESS_INTERVAL_MS = 30000(30s) - Batch/concurrency:
MAX_CONCURRENT_JOBS = 3 - Fetches pending jobs and processes in parallel
- Routes by target and entity type (
routeLGLSyncfor LGL)
Retry and Status Lifecycle
Queue service constants in src/services/sync-queue.ts:
MAX_RETRY_ATTEMPTS = 3RETRY_DELAY_MS = 5000
Lifecycle:
- New job ->
queued - Worker picks job ->
running - Success ->
succeeded - Error ->
failed(and retries according to attempts policy / retry endpoint)
Manual retry:
- Admin endpoint resets failed jobs back to queue flow (
POST /admin/sync/retry/:jobId)
LGL API Rate Limiting Interaction
Queue controls job concurrency; LGL client adds request-level controls:
- hard cap window: 300 requests / 5 minutes
- minimum inter-request delay: ~1.1 seconds
- availability wait when near cap
This creates two layers of throughput control:
- job-level (
sync-queue-processor) - request-level (
src/clients/lgl.tsRateLimiter)
4) Admin UI — What Staff Can See and Do
/admin/sync
UI file: admin/app/admin/sync/page.tsx
What staff sees:
- Sync jobs table across targets (LGL/WordPress/etc.)
- Filters by status (all/pending/failed)
- Columns include target, entity type, status, attempts, last error, timestamps
What staff can do:
- Retry failed jobs (
POST /api/admin/sync/retry/:jobId) - Navigate to LGL-specific sync view and other target views
/admin/sync/lgl
UI file: admin/app/admin/sync/lgl/page.tsx
What staff sees:
- Connection status and rate-limit summary
- Manual payment sync controls by payment ID
- Pending vs synced payment tabs with details
- Test scenario utilities (create/cleanup test records)
- Additional tabs for users, memberships, relationships, and LGL queue summaries
What staff can do:
- Test LGL connectivity (
GET /api/admin/lgl/test-connection) - Trigger immediate payment sync (
POST /api/admin/lgl/sync/payment/:paymentId) - Queue payment sync (
POST /api/admin/lgl/sync/queue-payment/:paymentId) - Force re-sync in test mode where exposed
- Trigger user/membership/relationship sync actions via dedicated endpoints
- Create/delete LGL test scenarios/data
/admin/system/lgl (LGL Sync Hub)
UI file: admin/app/admin/system/lgl/page.tsx
What staff sees:
- Connection banner (API key configured, environment/base URL cues, rate limit health)
- Tabbed views:
- Users (LGL constituent linkage + last sync)
- Payments (pending/synced, gift IDs)
- Memberships (membership linkage/status)
- Relationships (relationship linkage/status)
- Queue (LGL-only jobs, status/attempts/errors)
What staff can do:
- Test connection
- Sync/re-sync users manually
- Sync pending payments manually or queue them
- Sync memberships manually
- Queue relationship sync
- Refresh queue data and jump to full system queue page
/admin/settings/lgl
UI file: admin/app/admin/settings/lgl/page.tsx
What staff sees:
- Environment selector (sandbox/production)
- API key/base URL config controls
- Payment gateway mapping editor
- Membership level mapping editor
- Relationship type enable/default controls
- Sync behavior toggles
What staff can do:
- Save API config (
POST /api/admin/lgl/config/api) - Switch environment (
POST /api/admin/lgl/environment) - Test connection
- Load/refresh LGL payment types and save gateway mappings
- Load/refresh LGL membership levels and save product mappings
- Run mapping maintenance actions (product ID mismatch fixer / migration helper)
- Load/refresh relationship types and save enabled/default configuration
- Save sync toggles (
auto_sync,sync_on_payment,sync_on_membership)
Connection Test Implementation
Connection tests call:
- Admin API:
GET /admin/lgl/test-connection - Service:
testLGLConnection()insrc/services/lgl-sync.ts - Client:
testConnection()insrc/clients/lgl.ts(read request against LGL constituents endpoint)
Response includes:
- success/failure status
- configured state
- rate-limit metadata when available
5) LGL API Integration Layer
Client Location and Responsibility
src/clients/lgl.tscontains the complete integration adapter for all LGL HTTP operations.- It centralizes auth header creation, environment/base URL selection, retries, and response parsing.
Authentication
- Header:
Authorization: Bearer <apiKey> - API key source precedence:
lgl_settings(api_config) vialglSettingsRepoprocess.env.LGL_API_KEYfallback
- Base URL source precedence:
lgl_settings(api_config)process.env.LGL_API_BASE_URLfallback
LGL Endpoints Called (By Capability)
Constituents/contact:
- constituents list/search endpoints (name-based query)
- create/update constituent endpoint
- constituent email/phone/address subresource endpoints (delete + add pattern)
Gifts:
- create gift endpoint for generic and category-specific wrappers (
membership,class,event)
Membership:
- list constituent memberships
- patch existing membership
- create membership
Relationships:
- list relationship types
- create constituent relationship
- list/delete constituent relationships (when needed)
Reference/config pull:
- payment types list
- membership levels list
Error Handling
request() in lgl.ts:
- Parses diverse error response structures into a normalized message when possible.
- Marks specific statuses retryable:
429,500,502,503,504. - Non-retryable 4xx errors surface immediately.
Retry / Backoff
API-level retry defaults:
- max retries: 3
- base delay: 1000ms
- exponential backoff progression
Queue-level retries are separate (job lifecycle retries in sync-queue).
Rate Limiting Compliance
RateLimiter in lgl.ts enforces:
- max 300 requests / 300 seconds
- minimum spacing between calls (~1100ms)
- wait-for-availability when at/near cap
This makes LGL request pacing independent of queue burst behavior.
6) Configuration
Environment Variables
Used by LGL sync stack (with DB override patterns):
LGL_API_KEYLGL_API_BASE_URLLGL_ENVIRONMENT(sandbox/production)LGL_GROUP_ID_SCHOLARSHIP_FULLLGL_GROUP_ID_SCHOLARSHIP_PARTIAL
Database Configuration (lgl_settings)
Stored/retrieved through lglSettingsRepo:
- API config (
api_config: key/base URL/environment) - Fund mappings (by sync category, includes fund ID + campaign)
- Payment gateway -> LGL payment type mappings
- Membership product/level -> LGL membership level mappings
- Relationship type configuration (enabled set + default ID)
- Sync behavior flags (auto-sync and trigger-specific toggles)
- Cached LGL reference data (relationship types/payment types/membership levels)
Admin-Configurable Mapping Areas
- Fund/campaign mapping (membership, language classes, events, donations)
- Gateway payment type mapping (Stripe and others as available)
- Membership level mapping per product/global default
- Relationship type enable/default selections
Hardcoded or Semi-Hardcoded Values to Note
- Queue defaults: 30s poll, 3 concurrent jobs
- Queue retry defaults: 3 attempts, 5s retry delay
- LGL client retry defaults: 3 attempts, 1s base delay, specific retryable statuses
- LGL rate limiter constants: 300/5min + 1.1s min delay
- Constituent defaults (contact type and naming defaults) inside
createConstituent - Legacy relationship fallback behavior exists when no relationship type ID is configured
7) Known Edge Cases and Failure Modes
Constituent Matching / Dedup Risks
- LGL API cannot do direct email query; matching relies on name query plus client-side email filter.
- Same-name constituents with different emails can still be ambiguous in edge populations.
- If local linkage exists but email mismatch is detected, linkage is cleared and a new/matching flow runs (may produce additional constituents if source data is inconsistent).
Invalid or Missing API Configuration
- Missing/invalid API key or base URL causes connection test and sync requests to fail early.
- Admin UI surfaces configured/not-configured state and provides test endpoint, but background jobs can still fail until config is corrected.
LGL Downtime / 5xx / Rate-Limit Responses
- Request-level retries absorb transient outages (
429/5xx), but persistent outages still push jobs to failed state. - If failures exceed retry windows, manual retry or operational intervention is required.
Persistent Job Failures
- Jobs can remain failed due to bad entity references, mapping gaps, invalid relationship type IDs, or upstream API errors.
- Staff must use queue views and retry actions after correcting root cause.
Enrollment/Event Representation
- Enrollment sync is mostly audit/status level; revenue record in LGL is driven by payment->gift path.
- Missing or unusual enrollment-product associations can route fallback gift logic (e.g., defaulting toward membership fund path).
Membership State Drift Scenarios
- Membership sync intentionally deactivates existing LGL memberships before creating current one; mis-timed local date fields can temporarily reflect unexpected finish dates.
Relationship Type Configuration Gaps
- If no relationship type ID is configured/available, code can fall back to deprecated relationship helper behavior.
Noted TODO/FIXME/Deprecation Signals
In src/clients/lgl.ts:
findConstituentByEmailis explicitly deprecated/broken for real matching use.- older relationship helper methods are deprecated in favor of
createConstituentRelationship.
8) Data Flow Diagram (text-based)
End-to-End Sync Flow
text
[Member Action in Portal/Admin]
|
|-- checkout/membership | checkout/enrollment | checkout/cart | scholarship flow
v
[Local DB Writes]
- payment (pending/succeeded)
- payment_items (cart)
- membership/enrollment/family records as applicable
|
v
[Stripe Event or Direct Zero-$ Completion]
|
|-- webhooks.ts handles event
| - verifies signature
| - dedupes event
| - updates payment state
| - creates/updates membership/enrollment outcomes
| - queues sync jobs (target=lgl, entity_type=...)
v
[sync_jobs table]
status: queued -> running -> succeeded/failed
attempts increment on failures/retries
|
v
[sync-queue-processor (30s interval, up to 3 jobs)]
|
|-- routeLGLSync(entity_type)
| -> lgl-sync.ts
| - syncUserToLGL
| - syncPaymentToLGL
| - syncMembershipToLGL
| - syncEnrollmentToLGL
| - syncFamilyRelationshipToLGL
v
[lgl.ts client request()]
- loads API config (DB/env)
- applies rate limiting (300/5min, 1.1s min delay)
- calls LGL endpoints
- retries transient 429/5xx
|
+--> [Success]
| - update local lgl_* linkage ids
| - write audit logs
| - mark job succeeded
|
+--> [Failure]
- capture error
- mark failed/retry path
- visible in Admin Queue UI for manual retryStaff Observation and Intervention Loop
text
Admin UI (/admin/system/lgl, /admin/sync, /admin/settings/lgl)
|
|-- observe queue failures, unsynced entities, connection health
|-- test connection
|-- fix config/mappings
|-- retry or manually trigger sync
v
System re-enters queue/client flow until jobs succeed