feat(api): update DeviceLocation model and clean up AuthRepositoryImpl and BillingPageSkeleton

This commit is contained in:
Achintha Isuru
2026-03-18 10:00:27 -04:00
parent f218be44e2
commit 0b5254311d
5 changed files with 25 additions and 408 deletions

View File

@@ -2,6 +2,14 @@ import 'package:equatable/equatable.dart';
/// Represents a geographic location obtained from the device. /// Represents a geographic location obtained from the device.
class DeviceLocation extends Equatable { class DeviceLocation extends Equatable {
/// Creates a [DeviceLocation] instance.
const DeviceLocation({
required this.latitude,
required this.longitude,
required this.accuracy,
required this.timestamp,
});
/// Latitude in degrees. /// Latitude in degrees.
final double latitude; final double latitude;
@@ -14,14 +22,6 @@ class DeviceLocation extends Equatable {
/// Time when this location was determined. /// Time when this location was determined.
final DateTime timestamp; final DateTime timestamp;
/// Creates a [DeviceLocation] instance.
const DeviceLocation({
required this.latitude,
required this.longitude,
required this.accuracy,
required this.timestamp,
});
@override @override
List<Object?> get props => [latitude, longitude, accuracy, timestamp]; List<Object?> get props => <Object?>[latitude, longitude, accuracy, timestamp];
} }

View File

@@ -10,7 +10,6 @@ import 'package:krow_domain/krow_domain.dart'
AppException, AppException,
BaseApiService, BaseApiService,
ClientSession, ClientSession,
InvalidCredentialsException,
NetworkException, NetworkException,
PasswordMismatchException, PasswordMismatchException,
SignInFailedException, SignInFailedException,
@@ -28,7 +27,7 @@ import 'package:krow_domain/krow_domain.dart'
class AuthRepositoryImpl implements AuthRepositoryInterface { class AuthRepositoryImpl implements AuthRepositoryInterface {
/// Creates an [AuthRepositoryImpl] with the given [BaseApiService]. /// Creates an [AuthRepositoryImpl] with the given [BaseApiService].
AuthRepositoryImpl({required BaseApiService apiService}) AuthRepositoryImpl({required BaseApiService apiService})
: _apiService = apiService; : _apiService = apiService;
/// The V2 API service for backend calls. /// The V2 API service for backend calls.
final BaseApiService _apiService; final BaseApiService _apiService;
@@ -46,23 +45,16 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// via Identity Toolkit and returns a full auth envelope. // via Identity Toolkit and returns a full auth envelope.
final ApiResponse response = await _apiService.post( final ApiResponse response = await _apiService.post(
AuthEndpoints.clientSignIn, AuthEndpoints.clientSignIn,
data: <String, dynamic>{ data: <String, dynamic>{'email': email, 'password': password},
'email': email,
'password': password,
},
); );
final Map<String, dynamic> body = final Map<String, dynamic> body = response.data as Map<String, dynamic>;
response.data as Map<String, dynamic>;
// Step 2: Sign in locally so AuthInterceptor can attach Bearer tokens // Step 2: Sign in locally so AuthInterceptor can attach Bearer tokens
// to subsequent requests. The V2 API already validated credentials, so // to subsequent requests. The V2 API already validated credentials, so
// email/password sign-in establishes the local Firebase Auth state. // email/password sign-in establishes the local Firebase Auth state.
final firebase.UserCredential credential = final firebase.UserCredential credential = await _auth
await _auth.signInWithEmailAndPassword( .signInWithEmailAndPassword(email: email, password: password);
email: email,
password: password,
);
final firebase.User? firebaseUser = credential.user; final firebase.User? firebaseUser = credential.user;
if (firebaseUser == null) { if (firebaseUser == null) {
@@ -106,11 +98,8 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// Step 2: Sign in locally to Firebase Auth so AuthInterceptor works // Step 2: Sign in locally to Firebase Auth so AuthInterceptor works
// for subsequent requests. The V2 API already created the Firebase // for subsequent requests. The V2 API already created the Firebase
// account, so this should succeed. // account, so this should succeed.
final firebase.UserCredential credential = final firebase.UserCredential credential = await _auth
await _auth.signInWithEmailAndPassword( .signInWithEmailAndPassword(email: email, password: password);
email: email,
password: password,
);
final firebase.User? firebaseUser = credential.user; final firebase.User? firebaseUser = credential.user;
if (firebaseUser == null) { if (firebaseUser == null) {
@@ -155,10 +144,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// Step 1: Call V2 sign-out endpoint for server-side token revocation. // Step 1: Call V2 sign-out endpoint for server-side token revocation.
await _apiService.post(AuthEndpoints.clientSignOut); await _apiService.post(AuthEndpoints.clientSignOut);
} catch (e) { } catch (e) {
developer.log( developer.log('V2 sign-out request failed: $e', name: 'AuthRepository');
'V2 sign-out request failed: $e',
name: 'AuthRepository',
);
// Continue with local sign-out even if server-side fails. // Continue with local sign-out even if server-side fails.
} }
@@ -202,14 +188,14 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
'userId': userJson['id'] ?? userJson['userId'], 'userId': userJson['id'] ?? userJson['userId'],
}, },
}; };
final ClientSession clientSession = final ClientSession clientSession = ClientSession.fromJson(
ClientSession.fromJson(normalisedEnvelope); normalisedEnvelope,
);
ClientSessionStore.instance.setSession(clientSession); ClientSessionStore.instance.setSession(clientSession);
} }
final String userId = final String userId = userJson?['id'] as String? ?? firebaseUser.uid;
userJson?['id'] as String? ?? firebaseUser.uid; final String email = userJson?['email'] as String? ?? fallbackEmail;
final String? email = userJson?['email'] as String? ?? fallbackEmail;
return User( return User(
id: userId, id: userId,

View File

@@ -19,7 +19,7 @@ class BillingPageSkeleton extends StatelessWidget {
padding: const EdgeInsets.all(UiConstants.space5), padding: const EdgeInsets.all(UiConstants.space5),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: <Widget>[
// Pending invoices section header // Pending invoices section header
const UiShimmerSectionHeader(), const UiShimmerSectionHeader(),
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
@@ -39,7 +39,7 @@ class BillingPageSkeleton extends StatelessWidget {
), ),
child: const Column( child: const Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: <Widget>[
UiShimmerLine(width: 160, height: 16), UiShimmerLine(width: 160, height: 16),
SizedBox(height: UiConstants.space4), SizedBox(height: UiConstants.space4),
// Breakdown rows // Breakdown rows

View File

@@ -10,7 +10,7 @@ class BreakdownRowSkeleton extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Row( return const Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: <Widget>[
UiShimmerLine(width: 100, height: 14), UiShimmerLine(width: 100, height: 14),
UiShimmerLine(width: 60, height: 14), UiShimmerLine(width: 60, height: 14),
], ],

View File

@@ -1,369 +0,0 @@
# KROW Workforce API Contracts
Legacy note:
Use `/Users/wiel/Development/krow-workforce/docs/BACKEND/API_GUIDES/V2/README.md` for the current v2 frontend/backend integration surface.
This document reflects the earlier Data Connect-oriented contract mapping and should not be the source of truth for new v2 client work.
This document captures all API contracts used by the Staff and Client mobile applications. The application backend is powered by **Firebase Data Connect (GraphQL)**, so traditional REST endpoints do not exist natively. For clarity and ease of reading for all engineering team members, the tables below formulate these GraphQL Data Connect queries and mutations into their **Conceptual REST Endpoints** alongside the actual **Data Connect Operation Name**.
---
## Staff Application
### Authentication / Onboarding Pages
*(Pages: get_started_page.dart, intro_page.dart, phone_verification_page.dart, profile_setup_page.dart)*
#### Setup / User Validation API
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /users/{id}` |
| **Data Connect OP** | `getUserById` |
| **Purpose** | Retrieves the base user profile to determine authentication status and role access (e.g., if the user is STAFF). |
| **Operation** | Query |
| **Inputs** | `id: UUID!` (Firebase UID) |
| **Outputs** | `User { id, email, phone, role }` |
| **Notes** | Required after OTP verification to route users appropriately. |
#### Create Default User API
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `POST /users` |
| **Data Connect OP** | `createUser` |
| **Purpose** | Inserts a base user record into the system during initial signup. |
| **Operation** | Mutation |
| **Inputs** | `id: UUID!`, `role: UserBaseRole` |
| **Outputs** | `id` of newly created User |
| **Notes** | Used explicitly during the "Sign Up" flow if the user doesn't physically exist in the database. |
#### Get Staff Profile API
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /staff/user/{userId}` |
| **Data Connect OP** | `getStaffByUserId` |
| **Purpose** | Finds the specific Staff record associated with the base user ID. |
| **Operation** | Query |
| **Inputs** | `userId: UUID!` |
| **Outputs** | `Staffs { id, userId, fullName, email, phone, photoUrl, status }` |
| **Notes** | Needed to verify if a complete staff profile exists before allowing navigation to the main app dashboard. |
#### Update Staff Profile API
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `PUT /staff/{id}` |
| **Data Connect OP** | `updateStaff` |
| **Purpose** | Saves onboarding data across Personal Info, Experience, and Preferred Locations pages. |
| **Operation** | Mutation |
| **Inputs** | `id: UUID!`, `fullName`, `email`, `phone`, `address`, etc. |
| **Outputs** | `id` |
| **Notes** | Called incrementally during the profile setup wizard as the user fills out step-by-step information. |
### Home Page & Benefits Overview
*(Pages: worker_home_page.dart, benefits_overview_page.dart)*
#### Load Today/Tomorrow Shifts
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /staff/{staffId}/applications` |
| **Data Connect OP** | `getApplicationsByStaffId` |
| **Purpose** | Retrieves applications (shifts) assigned to the current staff member within a specific date range. |
| **Operation** | Query |
| **Inputs** | `staffId: UUID!`, `dayStart: Timestamp`, `dayEnd: Timestamp` |
| **Outputs** | `Applications { shift, shiftRole, status, createdAt }` |
| **Notes** | The frontend filters the query response for `CONFIRMED` applications to successfully display "Today's" and "Tomorrow's" shifts. |
#### List Recommended Shifts
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /shifts/recommended` |
| **Data Connect OP** | `listShifts` |
| **Purpose** | Fetches open shifts that are available for the staff to apply to. |
| **Operation** | Query |
| **Inputs** | None directly mapped on load, but fetches available items logically. |
| **Outputs** | `Shifts { id, title, orderId, cost, location, startTime, endTime, status }` |
| **Notes** | Limits output to 10 on the frontend. Should ideally rely on an active backend `$status: OPEN` parameter. |
#### Benefits Summary API
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /staff/{staffId}/benefits` |
| **Data Connect OP** | `listBenefitsDataByStaffId` |
| **Purpose** | Retrieves accrued benefits (e.g., Sick time, Vacation) to display gracefully on the home screen. |
| **Operation** | Query |
| **Inputs** | `staffId: UUID!` |
| **Outputs** | `BenefitsDatas { vendorBenefitPlan { title, total }, current }` |
| **Notes** | Used by `benefits_overview_page.dart`. Derives available metrics via `usedHours = total - current`. |
### Find Shifts / Shift Details Pages
*(Pages: shifts_page.dart, shift_details_page.dart)*
#### List Available Shifts Filtered
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /shifts` |
| **Data Connect OP** | `filterShifts` |
| **Purpose** | Used to fetch Open Shifts in specific regions when the worker searches in the "Find Shifts" tab. |
| **Operation** | Query |
| **Inputs** | `$status: ShiftStatus`, `$dateFrom: Timestamp`, `$dateTo: Timestamp` |
| **Outputs** | `Shifts { id, title, location, cost, durationDays, order { business, vendor } }` |
| **Notes** | Main driver for discovering available work. |
#### Get Shift Details
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /shifts/{id}` |
| **Data Connect OP** | `getShiftById` |
| **Purpose** | Gets deeper details for a single shift including exact uniform requirements and managers. |
| **Operation** | Query |
| **Inputs** | `id: UUID!` |
| **Outputs** | `Shift { id, title, hours, cost, locationAddress, workersNeeded ... }` |
| **Notes** | Invoked when users click into a full `shift_details_page.dart`. |
#### Apply To Shift
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `POST /applications` |
| **Data Connect OP** | `createApplication` |
| **Purpose** | Worker submits an intent to take an open shift (creates an application record). |
| **Operation** | Mutation |
| **Inputs** | `shiftId: UUID!`, `staffId: UUID!`, `roleId: UUID!`, `status: ApplicationStatus!` (e.g. `PENDING` or `CONFIRMED`), `origin: ApplicationOrigin!` (e.g. `STAFF`); optional: `checkInTime`, `checkOutTime` |
| **Outputs** | `application_insert.id` (Application ID) |
| **Notes** | The app uses `status: CONFIRMED` and `origin: STAFF` when claiming; backend also supports `PENDING` for admin review flows. After creation, shift-role assigned count and shift filled count are updated. |
### Availability Page
*(Pages: availability_page.dart)*
#### Get Default Availability
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /staff/{staffId}/availabilities` |
| **Data Connect OP** | `listStaffAvailabilitiesByStaffId` |
| **Purpose** | Fetches the standard Mon-Sun recurring availability for a staff member. |
| **Operation** | Query |
| **Inputs** | `staffId: UUID!` |
| **Outputs** | `StaffAvailabilities { dayOfWeek, isAvailable, startTime, endTime }` |
| **Notes** | Bound to Monday through Sunday configuration. |
#### Update Availability
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `PUT /staff/availabilities/{id}` |
| **Data Connect OP** | `updateStaffAvailability` (or `createStaffAvailability` for new entries) |
| **Purpose** | Upserts availability preferences. |
| **Operation** | Mutation |
| **Inputs** | `staffId`, `dayOfWeek`, `isAvailable`, `startTime`, `endTime` |
| **Outputs** | `id` |
| **Notes** | Called individually per day edited. |
### Payments Page
*(Pages: payments_page.dart, early_pay_page.dart)*
#### Get Recent Payments
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /staff/{staffId}/payments` |
| **Data Connect OP** | `listRecentPaymentsByStaffId` |
| **Purpose** | Loads the history of earnings and timesheets completed by the staff. |
| **Operation** | Query |
| **Inputs** | `staffId: UUID!` |
| **Outputs** | `Payments { amount, processDate, shiftId, status }` |
| **Notes** | Displays historical metrics under the comprehensive Earnings tab. |
### Compliance / Profiles
*(Pages: certificates_page.dart, documents_page.dart, tax_forms_page.dart, form_i9_page.dart, form_w4_page.dart)*
#### Get Tax Forms
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /staff/{staffId}/tax-forms` |
| **Data Connect OP** | `getTaxFormsByStaffId` |
| **Purpose** | Check the filing status and detailed inputs of I9 and W4 forms. |
| **Operation** | Query |
| **Inputs** | `staffId: UUID!` |
| **Outputs** | `TaxForms { formType, isCompleted, updatedDate }` |
| **Notes** | Crucial requirement for staff to be eligible to apply for highly regulated shifts. |
#### Update Tax Forms
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `PUT /tax-forms/{id}` |
| **Data Connect OP** | `updateTaxForm` |
| **Purpose** | Submits state and filing for the given tax form type (W4/I9). |
| **Operation** | Mutation |
| **Inputs** | `id`, `dataPoints...` |
| **Outputs** | `id` |
| **Notes** | Modifies the core compliance state variables directly. |
---
## Client Application
### Authentication / Intro
*(Pages: client_sign_in_page.dart, client_get_started_page.dart)*
#### Client User Validation API
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /users/{id}` |
| **Data Connect OP** | `getUserById` |
| **Purpose** | Retrieves the base user profile to determine authentication status and role access (ensuring user is BUSINESS). |
| **Operation** | Query |
| **Inputs** | `id: UUID!` (Firebase UID) |
| **Outputs** | `User { id, email, phone, userRole }` |
| **Notes** | Validates against conditional statements checking `userRole == BUSINESS` or `BOTH`. |
#### Get Businesses By User API
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /business/user/{userId}` |
| **Data Connect OP** | `getBusinessesByUserId` |
| **Purpose** | Maps the authenticated user to their client business context. |
| **Operation** | Query |
| **Inputs** | `userId: String!` |
| **Outputs** | `Businesses { id, businessName, email, contactName }` |
| **Notes** | Dictates the working scopes (Business ID) across the entire application lifecycle and binds the user. |
### Hubs Page
*(Pages: client_hubs_page.dart, edit_hub_page.dart, hub_details_page.dart)*
#### List Hubs by Team
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /teams/{teamId}/hubs` |
| **Data Connect OP** | `getTeamHubsByTeamId` |
| **Purpose** | Fetches the primary working sites (Hubs) for a client context by using Team mapping. |
| **Operation** | Query |
| **Inputs** | `teamId: UUID!` |
| **Outputs** | `TeamHubs { id, hubName, address, managerName, isActive }` |
| **Notes** | `teamId` is derived first from `getTeamsByOwnerId(ownerId: businessId)`. |
#### Create / Update / Delete Hub
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `POST /team-hubs` / `PUT /team-hubs/{id}` / `DELETE /team-hubs/{id}` |
| **Data Connect OP** | `createTeamHub` / `updateTeamHub` / `deleteTeamHub` |
| **Purpose** | Provisions, Edits details directly, or Removes a Team Hub location. |
| **Operation** | Mutation |
| **Inputs** | `id: UUID!`, optionally `hubName`, `address`, etc. |
| **Outputs** | `id` |
| **Notes** | Fired from `edit_hub_page.dart` mutations. |
### Orders Page
*(Pages: create_order_page.dart, view_orders_page.dart, recurring_order_page.dart)*
#### Create Order
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `POST /orders` |
| **Data Connect OP** | `createOrder` |
| **Purpose** | Submits a new request for temporary staff requirements. |
| **Operation** | Mutation |
| **Inputs** | `businessId`, `eventName`, `orderType`, `status` |
| **Outputs** | `id` (Order ID) |
| **Notes** | This explicitly invokes an order pipeline, meaning Shift instances are subsequently created through secondary mutations triggered after order instantiation. |
#### List Orders
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /business/{businessId}/orders` |
| **Data Connect OP** | `listOrdersByBusinessId` |
| **Purpose** | Retrieves all ongoing and past staff requests from the client. |
| **Operation** | Query |
| **Inputs** | `businessId: UUID!` |
| **Outputs** | `Orders { id, eventName }` |
| **Notes** | Populates the `view_orders_page.dart`. |
### Billing Pages
*(Pages: billing_page.dart, pending_invoices_page.dart, completion_review_page.dart)*
#### List Invoices
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /business/{businessId}/invoices` |
| **Data Connect OP** | `listInvoicesByBusinessId` |
| **Purpose** | Fetches all invoices bound directly to the active business context (mapped directly in Firebase Schema). |
| **Operation** | Query |
| **Inputs** | `businessId: UUID!` |
| **Outputs** | `Invoices { id, amount, issueDate, status }` |
| **Notes** | Used massively across all Billing view tabs. |
#### Mark / Dispute Invoice
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `PUT /invoices/{id}` |
| **Data Connect OP** | `updateInvoice` |
| **Purpose** | Actively marks an invoice as disputed or pays it directly (altering status). |
| **Operation** | Mutation |
| **Inputs** | `id: UUID!`, `status: InvoiceStatus` |
| **Outputs** | `id` |
| **Notes** | Disputing usually involves setting a `disputeReason` flag state dynamically via builder pattern. |
### Reports Page
*(Pages: reports_page.dart, coverage_report_page.dart, performance_report_page.dart)*
#### Get Coverage Stats
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /business/{businessId}/coverage` |
| **Data Connect OP** | `listShiftsForCoverage` |
| **Purpose** | Provides data on Shifts grouped by Date for fulfillment calculations. |
| **Operation** | Query |
| **Inputs** | `businessId: UUID!`, `startDate: Timestamp!`, `endDate: Timestamp!` |
| **Outputs** | `Shifts { id, date, workersNeeded, filled, status }` |
| **Notes** | The frontend aggregates the raw backend rows to compose Coverage percentage natively. |
#### Get Daily Ops Stats
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /business/{businessId}/dailyops` |
| **Data Connect OP** | `listShiftsForDailyOpsByBusiness` |
| **Purpose** | Supplies current day operations and shift tracking progress. |
| **Operation** | Query |
| **Inputs** | `businessId: UUID!`, `date: Timestamp!` |
| **Outputs** | `Shifts { id, title, location, workersNeeded, filled }` |
| **Notes** | - |
#### Get Forecast Stats
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /business/{businessId}/forecast` |
| **Data Connect OP** | `listShiftsForForecastByBusiness` |
| **Purpose** | Retrieves scheduled future shifts to calculate financial run-rates. |
| **Operation** | Query |
| **Inputs** | `businessId: UUID!`, `startDate: Timestamp!`, `endDate: Timestamp!` |
| **Outputs** | `Shifts { id, date, workersNeeded, hours, cost }` |
| **Notes** | The App maps hours `x` cost to deliver Financial Dashboards. |
#### Get Performance KPIs
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /business/{businessId}/performance` |
| **Data Connect OP** | `listShiftsForPerformanceByBusiness` |
| **Purpose** | Fetches historical data allowing time-to-fill and completion-rate calculations. |
| **Operation** | Query |
| **Inputs** | `businessId: UUID!`, `startDate: Timestamp!`, `endDate: Timestamp!` |
| **Outputs** | `Shifts { id, workersNeeded, filled, createdAt, filledAt }` |
| **Notes** | Data Connect exposes timestamps so the App calculates `avgFillTimeHours`. |
#### Get No-Show Metrics
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /business/{businessId}/noshows` |
| **Data Connect OP** | `listShiftsForNoShowRangeByBusiness` |
| **Purpose** | Retrieves shifts where workers historically ghosted the platform. |
| **Operation** | Query |
| **Inputs** | `businessId: UUID!`, `startDate: Timestamp!`, `endDate: Timestamp!` |
| **Outputs** | `Shifts { id, date }` |
| **Notes** | Accompanies `listApplicationsForNoShowRange` cascading querying to generate full report. |
#### Get Spend Analytics
| Field | Description |
|---|---|
| **Conceptual Endpoint** | `GET /business/{businessId}/spend` |
| **Data Connect OP** | `listInvoicesForSpendByBusiness` |
| **Purpose** | Detailed invoice aggregates for Spend metrics filtering. |
| **Operation** | Query |
| **Inputs** | `businessId: UUID!`, `startDate: Timestamp!`, `endDate: Timestamp!` |
| **Outputs** | `Invoices { id, issueDate, dueDate, amount, status }` |
| **Notes** | Used explicitly under the "Spend Report" graphings. |
---