Skip to main content

Accounting Integrations

Automate financial data sync with accounting platforms.

Overview

Olympus Cloud integrates with popular accounting software:

PlatformFeaturesSync Type
QuickBooks OnlineSales, Expenses, PaymentsReal-time
XeroSales, Expenses, BankReal-time
QuickBooks DesktopSales, ExpensesBatch
Custom ExportCSV, ExcelManual

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 CategoryQuickBooks AccountType
Food SalesFood RevenueIncome
Beverage SalesBeverage RevenueIncome
Tax CollectedSales Tax PayableLiability
TipsTips PayableLiability
Cash PaymentsUndeposited FundsAsset
Card PaymentsUndeposited FundsAsset
COGS - FoodCost of Goods Sold - FoodExpense
COGS - BeverageCost of Goods Sold - BeverageExpense

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(())
}