Accounting Integrations
Automate financial data sync with accounting platforms.
Overview
Olympus Cloud integrates with popular accounting software:
| Platform | Features | Sync Type |
|---|---|---|
| QuickBooks Online | Sales, Expenses, Payments | Real-time |
| Xero | Sales, Expenses, Bank | Real-time |
| QuickBooks Desktop | Sales, Expenses | Batch |
| Custom Export | CSV, Excel | Manual |
QuickBooks Online Integration
Setup
# config/integrations/quickbooks.yaml
quickbooks:
client_id: "your-client-id"
client_secret: "your-client-secret"
redirect_uri: "https://api.olympuscloud.ai/oauth/quickbooks/callback"
environment: production # or sandbox
OAuth Connection
// src/integrations/quickbooks/auth.rs
use oauth2::{AuthorizationCode, TokenResponse};
pub struct QuickBooksAuth {
client_id: String,
client_secret: String,
redirect_uri: String,
}
impl QuickBooksAuth {
pub fn get_authorization_url(&self, tenant_id: &str) -> String {
format!(
"https://appcenter.intuit.com/connect/oauth2?\
client_id={}&\
response_type=code&\
scope=com.intuit.quickbooks.accounting&\
redirect_uri={}&\
state={}",
self.client_id,
urlencoding::encode(&self.redirect_uri),
tenant_id
)
}
pub async fn exchange_code(
&self,
code: &str,
) -> Result<QuickBooksTokens> {
let client = reqwest::Client::new();
let response = client
.post("https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer")
.basic_auth(&self.client_id, Some(&self.client_secret))
.form(&[
("grant_type", "authorization_code"),
("code", code),
("redirect_uri", &self.redirect_uri),
])
.send()
.await?;
response.json().await
}
pub async fn refresh_token(
&self,
refresh_token: &str,
) -> Result<QuickBooksTokens> {
let client = reqwest::Client::new();
let response = client
.post("https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer")
.basic_auth(&self.client_id, Some(&self.client_secret))
.form(&[
("grant_type", "refresh_token"),
("refresh_token", refresh_token),
])
.send()
.await?;
response.json().await
}
}
Sync Sales
// src/integrations/quickbooks/sales.rs
pub struct QuickBooksSyncService {
client: QuickBooksClient,
}
impl QuickBooksSyncService {
pub async fn sync_daily_sales(
&self,
ctx: &TenantContext,
date: NaiveDate,
) -> Result<SyncResult> {
let tokens = self.get_tokens(ctx).await?;
// Get daily sales summary
let sales = sales_service.get_daily_summary(ctx, date).await?;
// Create sales receipt in QuickBooks
let receipt = SalesReceipt {
txn_date: date.format("%Y-%m-%d").to_string(),
line: vec![
Line {
amount: sales.food_sales,
detail_type: "SalesItemLineDetail",
sales_item_line_detail: SalesItemLineDetail {
item_ref: ItemRef {
value: self.get_food_item_id(ctx).await?,
},
},
},
Line {
amount: sales.beverage_sales,
detail_type: "SalesItemLineDetail",
sales_item_line_detail: SalesItemLineDetail {
item_ref: ItemRef {
value: self.get_beverage_item_id(ctx).await?,
},
},
},
],
deposit_to_account_ref: AccountRef {
value: self.get_deposit_account_id(ctx).await?,
},
};
self.client.create_sales_receipt(&tokens, &receipt).await
}
}
Create Invoice
pub async fn create_invoice(
&self,
ctx: &TenantContext,
order: &Order,
) -> Result<QuickBooksInvoice> {
let tokens = self.get_tokens(ctx).await?;
let customer = self.get_or_create_customer(ctx, &order.customer).await?;
let invoice = Invoice {
customer_ref: CustomerRef {
value: customer.id,
},
txn_date: order.created_at.format("%Y-%m-%d").to_string(),
due_date: (order.created_at + chrono::Duration::days(30))
.format("%Y-%m-%d").to_string(),
line: order.items.iter().map(|item| {
Line {
amount: item.total,
detail_type: "SalesItemLineDetail",
sales_item_line_detail: SalesItemLineDetail {
item_ref: ItemRef {
value: self.map_item_to_qb(item).await?,
},
qty: item.quantity as f64,
unit_price: item.unit_price,
},
}
}).collect(),
};
self.client.create_invoice(&tokens, &invoice).await
}
Account Mapping
| Olympus Category | QuickBooks Account | Type |
|---|---|---|
| Food Sales | Food Revenue | Income |
| Beverage Sales | Beverage Revenue | Income |
| Tax Collected | Sales Tax Payable | Liability |
| Tips | Tips Payable | Liability |
| Cash Payments | Undeposited Funds | Asset |
| Card Payments | Undeposited Funds | Asset |
| COGS - Food | Cost of Goods Sold - Food | Expense |
| COGS - Beverage | Cost of Goods Sold - Beverage | Expense |
Xero Integration
Setup
# config/integrations/xero.yaml
xero:
client_id: "your-client-id"
client_secret: "your-client-secret"
redirect_uri: "https://api.olympuscloud.ai/oauth/xero/callback"
scopes:
- openid
- profile
- email
- accounting.transactions
- accounting.settings
- accounting.contacts
OAuth Connection
// src/integrations/xero/auth.rs
pub struct XeroAuth {
client_id: String,
client_secret: String,
redirect_uri: String,
}
impl XeroAuth {
pub fn get_authorization_url(&self, tenant_id: &str) -> String {
let scopes = "openid profile email accounting.transactions accounting.settings";
format!(
"https://login.xero.com/identity/connect/authorize?\
response_type=code&\
client_id={}&\
redirect_uri={}&\
scope={}&\
state={}",
self.client_id,
urlencoding::encode(&self.redirect_uri),
urlencoding::encode(scopes),
tenant_id
)
}
}
Sync Invoices
// src/integrations/xero/invoices.rs
pub async fn sync_invoice(
&self,
ctx: &TenantContext,
order: &Order,
) -> Result<XeroInvoice> {
let tokens = self.get_tokens(ctx).await?;
let xero_tenant_id = self.get_xero_tenant_id(ctx).await?;
let invoice = XeroInvoice {
type_: "ACCREC",
contact: Contact {
contact_id: self.get_or_create_contact(ctx, &order.customer).await?,
},
date: order.created_at.format("%Y-%m-%d").to_string(),
due_date: (order.created_at + chrono::Duration::days(30))
.format("%Y-%m-%d").to_string(),
line_items: order.items.iter().map(|item| {
XeroLineItem {
description: item.name.clone(),
quantity: item.quantity as f64,
unit_amount: item.unit_price,
account_code: self.get_revenue_account(ctx).await?,
tax_type: "OUTPUT",
}
}).collect(),
status: "AUTHORISED",
};
self.client.create_invoice(&tokens, &xero_tenant_id, &invoice).await
}
Bank Feed
// src/integrations/xero/bank.rs
pub async fn sync_bank_transactions(
&self,
ctx: &TenantContext,
date: NaiveDate,
) -> Result<()> {
let tokens = self.get_tokens(ctx).await?;
let xero_tenant_id = self.get_xero_tenant_id(ctx).await?;
// Get payment summary
let payments = payment_service.get_daily_summary(ctx, date).await?;
// Create bank transaction for each payment type
for payment in payments {
let transaction = BankTransaction {
type_: "RECEIVE",
contact: Contact {
name: format!("{} Sales", date.format("%Y-%m-%d")),
},
bank_account: BankAccount {
account_id: self.get_bank_account_id(ctx, &payment.method).await?,
},
line_items: vec![XeroLineItem {
description: format!("{} payments", payment.method),
quantity: 1.0,
unit_amount: payment.total,
account_code: self.get_revenue_account(ctx).await?,
}],
date: date.format("%Y-%m-%d").to_string(),
};
self.client.create_bank_transaction(
&tokens,
&xero_tenant_id,
&transaction
).await?;
}
Ok(())
}
Sync Configuration
Sync Settings
# config/accounting_sync.yaml
sync:
# Automatic sync schedule
schedule:
daily_sales: "0 2 * * *" # 2 AM daily
payments: "0 3 * * *" # 3 AM daily
inventory: "0 4 * * 0" # 4 AM weekly
# Sync options
options:
create_customers: true
create_items: true
sync_inventory: false
detailed_line_items: true
# Account mapping
accounts:
sales_account: "4000"
tax_account: "2100"
deposit_account: "1000"
cogs_account: "5000"
Manual Sync
// src/integrations/accounting/sync.rs
pub async fn manual_sync(
ctx: &TenantContext,
platform: AccountingPlatform,
date_range: DateRange,
) -> Result<SyncResult> {
let mut result = SyncResult::default();
for date in date_range.iter_days() {
match platform {
AccountingPlatform::QuickBooks => {
let r = quickbooks.sync_daily_sales(ctx, date).await?;
result.merge(r);
}
AccountingPlatform::Xero => {
let r = xero.sync_daily_sales(ctx, date).await?;
result.merge(r);
}
}
}
Ok(result)
}
Data Mapping
Item Mapping
// src/integrations/accounting/mapping.rs
#[derive(Clone)]
pub struct AccountingMapping {
pub olympus_id: String,
pub external_id: String,
pub account_code: String,
}
pub async fn get_or_create_mapping(
ctx: &TenantContext,
platform: AccountingPlatform,
item: &MenuItem,
) -> Result<AccountingMapping> {
// Check existing mapping
if let Some(mapping) = mapping_store.get(ctx, &item.id, platform).await? {
return Ok(mapping);
}
// Create item in accounting system
let external_id = match platform {
AccountingPlatform::QuickBooks => {
quickbooks.create_item(ctx, item).await?.id
}
AccountingPlatform::Xero => {
xero.create_item(ctx, item).await?.item_id
}
};
// Save mapping
let mapping = AccountingMapping {
olympus_id: item.id.clone(),
external_id,
account_code: get_account_code(&item.category),
};
mapping_store.save(ctx, platform, &mapping).await?;
Ok(mapping)
}
Reports Export
CSV Export
// src/integrations/accounting/export.rs
pub async fn export_sales_csv(
ctx: &TenantContext,
date_range: DateRange,
) -> Result<Vec<u8>> {
let sales = sales_service.get_range(ctx, date_range).await?;
let mut writer = csv::Writer::from_writer(vec![]);
writer.write_record(&[
"Date", "Order ID", "Type", "Subtotal", "Tax", "Total",
"Payment Method", "Customer",
])?;
for sale in sales {
writer.write_record(&[
sale.date.format("%Y-%m-%d").to_string(),
sale.order_id,
sale.order_type.to_string(),
sale.subtotal.to_string(),
sale.tax.to_string(),
sale.total.to_string(),
sale.payment_method,
sale.customer_name.unwrap_or_default(),
])?;
}
Ok(writer.into_inner()?)
}
Excel Export
pub async fn export_financial_excel(
ctx: &TenantContext,
date_range: DateRange,
) -> Result<Vec<u8>> {
let workbook = Workbook::new();
// Sales Summary sheet
let mut sales_sheet = workbook.add_worksheet(Some("Sales Summary"))?;
let sales = sales_service.get_daily_summary(ctx, date_range).await?;
sales_sheet.write_string(0, 0, "Date")?;
sales_sheet.write_string(0, 1, "Gross Sales")?;
sales_sheet.write_string(0, 2, "Discounts")?;
sales_sheet.write_string(0, 3, "Net Sales")?;
sales_sheet.write_string(0, 4, "Tax")?;
sales_sheet.write_string(0, 5, "Total")?;
for (i, day) in sales.iter().enumerate() {
let row = (i + 1) as u32;
sales_sheet.write_string(row, 0, &day.date.to_string())?;
sales_sheet.write_number(row, 1, day.gross_sales)?;
sales_sheet.write_number(row, 2, day.discounts)?;
sales_sheet.write_number(row, 3, day.net_sales)?;
sales_sheet.write_number(row, 4, day.tax)?;
sales_sheet.write_number(row, 5, day.total)?;
}
// Add more sheets...
workbook.save_to_buffer()
}
Reconciliation
Daily Reconciliation
pub async fn reconcile_daily(
ctx: &TenantContext,
date: NaiveDate,
) -> Result<ReconciliationResult> {
let olympus_sales = sales_service.get_daily_total(ctx, date).await?;
let accounting_sales = accounting_service.get_daily_total(ctx, date).await?;
let difference = olympus_sales - accounting_sales;
if difference.abs() > 0.01 {
// Log discrepancy
audit_service.log_discrepancy(ctx, ReconciliationDiscrepancy {
date,
olympus_amount: olympus_sales,
accounting_amount: accounting_sales,
difference,
}).await?;
}
Ok(ReconciliationResult {
date,
olympus_total: olympus_sales,
accounting_total: accounting_sales,
matched: difference.abs() <= 0.01,
difference,
})
}
Error Handling
Sync Errors
pub async fn handle_sync_error(
ctx: &TenantContext,
error: AccountingSyncError,
) -> Result<()> {
match error {
AccountingSyncError::AuthExpired => {
// Notify user to reconnect
notification_service.send(ctx, Notification {
type_: NotificationType::AccountingReconnect,
message: "Your accounting connection has expired. Please reconnect.",
}).await?;
}
AccountingSyncError::RateLimited { retry_after } => {
// Schedule retry
scheduler.schedule_retry(ctx, retry_after).await?;
}
AccountingSyncError::ValidationError { message } => {
// Log and notify
audit_service.log_error(ctx, &message).await?;
}
}
Ok(())
}
Related Documentation
- Payments API - Payment processing
- Analytics API - Sales reports
- Webhooks - Event notifications