Back to Business KPI Dashboard

Developer Handover Pack

Everything needed to build the automation from scratch: current state, full agent specs, data flow, and the build stack.

4 pagesPDF · Technical
FS-DOC-04Technical

Developer Handover Pack

Business KPI Dashboard

[YourCompany.com] · Operations Department · Prepared by FullSpec · [Today's Date]

This document gives the FullSpec build team everything needed to construct, wire, and validate the Business KPI Dashboard automation. It covers the current-state process map with bottleneck analysis, full agent specifications with IO contracts, the end-to-end data flow, and the recommended build stack. The FullSpec team owns all build and integration work. Your team's responsibility during this phase is to confirm API credentials, validate KPI definitions, and approve the two live test cycles before go-live.

01Process snapshot

Step
Name
Description
1
Open Reporting Tracker Spreadsheet
Ops Manager opens the master Google Sheet and clears last week's figures from input cells. Time cost: 5 min.
2 BOTTLENECK
Pull Revenue and Invoice Data from Xero
Manager logs into Xero, navigates to reports, and manually notes total revenue, outstanding invoices, and cash position for the period. Time cost: 20 min.
3 BOTTLENECK
Pull Sales Pipeline Data from HubSpot
Manager logs into HubSpot, filters deals by reporting period, and records deal count, pipeline value, and conversion rate. Time cost: 20 min.
4
Collect Operational Metrics from Internal Logs
Any metrics not housed in a central tool are gathered manually from email threads, internal logs, or team check-ins. Time cost: 25 min.
5
Enter All Figures into Master Spreadsheet
All collected numbers are typed into designated cells in Google Sheets, where formulas calculate derived KPIs such as margin and growth rate. Time cost: 20 min.
6
Verify Calculations and Spot-Check Figures
Manager reviews calculated totals for obvious errors, cross-references key figures against source tools, and corrects discrepancies. Time cost: 15 min.
7
Update Looker Studio Dashboard Manually
If the dashboard does not auto-connect to the sheet, the manager refreshes the data source connection and checks that charts have updated. Time cost: 10 min.
8
Write Weekly KPI Summary Note
A brief written commentary is drafted in Notion, calling out notable changes, risks, or wins to accompany the raw dashboard numbers. Time cost: 20 min.
9
Distribute Report to Stakeholders via Slack
Manager pastes a summary and a link to the dashboard into the relevant Slack channel and tags key team members. Time cost: 5 min.
10
Archive Report in Notion
The completed KPI summary and a snapshot of key figures are copied into the Notion reporting archive for historical reference. Time cost: 10 min.
Time cost summary: Total manual time per cycle is 150 minutes (2 hours 30 minutes). At approximately 4 cycles per month this equals roughly 600 minutes (10 hours) per month, or about 6 hours per week across a 4-week reporting cadence. Steps 2, 3, 5, 8, 9, and 10 are fully replaced by the three agents. Step 4 (operational metrics) and step 1 (spreadsheet prep) remain manual. Steps 6 and 7 are retained as a human spot-check and dashboard verification. The automation eliminates approximately 100 minutes of tool-switching, data entry, writing, and distribution per cycle.
Developer Handover PackPage 1 of 4
FS-DOC-04Technical

02Agent specifications

Data Collection Agent

Fires on a fixed weekly schedule (Monday 07:00 local time by default) and connects to Xero and HubSpot via their respective REST APIs. It retrieves the financial and sales metrics for the current reporting period, normalises field values into a consistent schema, and writes each figure into the pre-mapped input cells of the master Google Sheet. The agent does not perform any calculation; it is a pure extract-and-write operation. On completion it emits a success status that triggers the KPI Commentary Agent. If any API call fails or returns unexpected null values, the agent halts, logs the error to the central error table, and sends an alert to the ops manager before any downstream agents fire.

Trigger
Time-based schedule: every Monday at 07:00. Can also be triggered manually by the ops manager via a webhook endpoint.
Tools
Xero (Financial API), HubSpot (CRM API), Google Sheets (Sheets API v4)
Replaces steps
Step 2 (Xero pull), Step 3 (HubSpot pull), Step 5 (manual data entry into Sheets)
Estimated build
10 hours, Moderate complexity
// Input
trigger_type: 'schedule' | 'webhook'
reporting_period_start: ISO8601 date (auto-calculated as Monday 00:00 UTC)
reporting_period_end:   ISO8601 date (auto-calculated as Sunday 23:59 UTC)

// Xero fields retrieved
xero.total_revenue:        number (GBP/USD, per organisation currency)
xero.outstanding_invoices: number (count)
xero.outstanding_value:    number (monetary)
xero.cash_position:        number (bank account balance, primary account)

// HubSpot fields retrieved
hubspot.deal_count:        integer (deals in 'Close Date' range)
hubspot.pipeline_value:    number (sum of deal amounts)
hubspot.conversion_rate:   float  (closed-won / total deals, period)

// Output
sheets.write_status:       'success' | 'partial' | 'failed'
sheets.cells_written:      integer (count of cells updated)
sheets.timestamp:          ISO8601 datetime
agent.status:              'complete' | 'error'
agent.error_detail:        string | null
  • Xero API tier: Requires the Accounting API scope (openid profile email accounting.reports.read accounting.transactions.read). OAuth 2.0 PKCE flow. Credentials must be provisioned as a Xero app under the client's organisation before build starts. Read-only scope only; confirm this is enforced at the app level.
  • HubSpot API tier: Requires a Private App token with scopes crm.objects.deals.read and crm.pipelines.read. Standard CRM plan or above. Confirm the client's HubSpot subscription supports Private Apps (Professional and Enterprise do; Starter may require OAuth instead).
  • Google Sheets API: Service account authentication via JSON key. The service account email must be granted Editor access to the master Sheet before the agent can write. Confirm sheet ID and tab name (default target tab: 'KPI_Input') before build.
  • Cell mapping: Each Xero and HubSpot field must be mapped to an explicit A1-notation cell reference in the KPI_Input tab. FullSpec will document this mapping in the Integration and API Spec. Any manual restructuring of the sheet after go-live breaks the mapping and requires a configuration update.
  • Dedupe and idempotency: If the schedule fires twice (e.g. after a retry), the agent must overwrite existing cell values rather than appending. The write operation is idempotent by design. Include a run_id (UUID) written to a dedicated audit cell (e.g. B1) so duplicate runs can be detected in logs.
  • Null and missing value handling: If a Xero or HubSpot field returns null or is outside an expected numeric range, the agent must NOT write the value. It must flag the field as anomalous, halt execution, and route to the anomaly alert flow. Define acceptable numeric ranges for each KPI before build (confirm with ops manager).
  • Confirmed before build: Xero OAuth app created and approved; HubSpot Private App token issued; Google Sheets service account granted Editor access; KPI cell mapping document signed off; anomaly thresholds per metric agreed in writing.
KPI Commentary Agent

Fires after the Data Collection Agent emits a 'complete' status confirming all cells are written. It reads the updated KPI values from Google Sheets, compares them against the prior period values stored in a separate history tab, identifies statistically notable changes (defined as movement outside a configured percentage threshold per KPI), and passes a structured prompt to the configured large language model to produce a concise plain-English commentary. The commentary is then written to the designated Notion page for the current reporting week. The agent does not publish to Slack; it emits a confirmation that triggers the Distribution Agent. If the Notion write fails, the agent retries twice before logging an error and halting the chain.

Trigger
Upstream event: Data Collection Agent emits agent.status = 'complete'. No time-based trigger on this agent.
Tools
Google Sheets (Sheets API v4, read), Notion (Notion API v1, write), LLM inference endpoint (OpenAI GPT-4o or equivalent, configurable)
Replaces steps
Step 8 (write KPI summary note in Notion), Step 10 (archive report in Notion)
Estimated build
9 hours, Moderate complexity
// Input
trigger_event:            'data_collection_complete'
sheets.kpi_input_range:   A1-notation range of current-period values
sheets.history_range:     A1-notation range of prior-period values (KPI_History tab)
comparison_threshold_pct: float (default 10.0, configurable per KPI)

// KPI values read
current.total_revenue:        number
current.outstanding_value:    number
current.cash_position:        number
current.deal_count:           integer
current.pipeline_value:       number
current.conversion_rate:      float
prior.total_revenue:          number
prior.pipeline_value:         number
prior.conversion_rate:        float

// LLM prompt fields
prompt.kpi_delta_summary:     string (structured JSON of changes passed to model)
prompt.tone_instruction:      string ('concise, factual, plain English, under 120 words')
prompt.format_instruction:    string ('3 to 5 bullet points; no markdown headers')

// Output
notion.page_id:               string (UUID of the created/updated Notion page)
notion.page_url:              string (shareable URL for downstream use)
notion.write_status:          'success' | 'failed'
commentary.text:              string (final published commentary body)
agent.status:                 'complete' | 'error'

// On approval (anomaly path)
anomaly_flag:                 boolean
anomaly_fields:               array of string (KPI names outside threshold)
ops_manager_review_required:  boolean (true if any anomaly_flag is true)
ops_manager_approved:         boolean (set by manual webhook response before commentary is drafted)
  • LLM provider: OpenAI GPT-4o via the Chat Completions API. API key stored in the shared credential store, not hardcoded. If the client prefers a different provider (e.g. Anthropic Claude), the prompt schema is provider-agnostic and the endpoint is swappable via config.
  • Notion API: Integration token scoped to the specific Notion workspace and reporting database. The integration must be invited to the target Notion page before build. Page template structure (title, date property, commentary body block) must be agreed before build; the agent writes to named block IDs.
  • History tab dependency: The Google Sheets KPI_History tab must exist and follow a consistent row-per-week schema before this agent can calculate deltas. If this tab does not exist, FullSpec will create it during the Data Collection Agent build phase.
  • Commentary baseline: The first two live cycles will have no prior-period data in KPI_History. The agent must handle this gracefully by generating a commentary that notes it is the baseline week rather than producing a delta comparison. This logic must be implemented as a conditional branch.
  • Anomaly gate: If any KPI delta exceeds the configured threshold, the agent must pause before calling the LLM and emit an ops_manager_review_required flag. The anomaly alert is sent via Slack DM to the ops manager (separate from the public channel post). The agent waits for a webhook-based approval response before continuing. Timeout after 4 hours defaults to halt (not continue).
  • Retry logic: Notion write retries twice with a 10-second delay between attempts. On third failure, log to error table and send an alert. Do not retry the LLM call; store the generated commentary text in a temp variable so it is not lost if only the Notion write fails.
  • Confirmed before build: Notion integration token issued; target page and block IDs confirmed; KPI_History tab schema agreed; anomaly threshold values per metric agreed; LLM provider and API key provisioned.
Developer Handover PackPage 2 of 4
FS-DOC-04Technical
Distribution Agent

Fires after the KPI Commentary Agent confirms the Notion page has been published successfully. It constructs a formatted Slack message containing the headline KPI figures, the AI-generated commentary text, and a direct link to the live Looker Studio dashboard. The message is posted to the configured Slack channel with stakeholder mentions. The agent reads the Notion page URL and commentary text from the upstream event payload; it does not re-query Notion or Google Sheets. This keeps the agent stateless and fast. On Slack API failure the agent retries once and then logs an error; it does not halt any upstream state since all data is already published.

Trigger
Upstream event: KPI Commentary Agent emits notion.write_status = 'success'. No time-based trigger.
Tools
Slack (Web API, chat.postMessage), Google Looker Studio (static dashboard URL, no API call required)
Replaces steps
Step 9 (distribute report to stakeholders via Slack)
Estimated build
5 hours, Simple complexity
// Input
trigger_event:            'commentary_published'
notion.page_url:          string (from upstream KPI Commentary Agent output)
commentary.text:          string (from upstream KPI Commentary Agent output)
looker_studio.dashboard_url: string (static, configured at build time)
slack.channel_id:         string (configured at build time, e.g. C0123ABCDEF)
slack.mention_user_ids:   array of string (Slack user IDs for tagging, configured at build time)

// Slack message fields constructed
message.headline:         string ('KPI Report: [Week ending DATE]')
message.kpi_summary:      string (top 3 headline figures formatted as plain text)
message.commentary_block: string (commentary.text, max 500 characters before truncation)
message.dashboard_link:   string (looker_studio.dashboard_url)
message.notion_link:      string (notion.page_url, labelled 'Full report in Notion')

// Output
slack.message_ts:         string (Slack message timestamp, for audit log)
slack.post_status:        'success' | 'failed'
agent.status:             'complete' | 'error'
  • Slack API: Requires a Slack App with the chat:write OAuth scope installed to the target workspace. The app must be invited to the target channel before it can post. Bot token (xoxb-) stored in the shared credential store. Confirm channel ID (not channel name, which can change) before build.
  • Looker Studio dashboard URL: This is a static string configured at build time. No API call is made to Looker Studio. Confirm the dashboard is set to 'Anyone with the link can view' or that all Slack channel members have been granted view access in Google Workspace.
  • Message formatting: Use Slack Block Kit for structured formatting. The message block schema must be version-pinned to avoid rendering regressions on Slack API updates. Keep the total message payload under 3,000 characters to avoid truncation.
  • Stakeholder mentions: Slack user IDs for tagging must be supplied as configuration values (not display names). Confirm the user ID list with the ops manager before go-live. The default is to tag the ops manager and the CEO.
  • Idempotency: If the Distribution Agent fires twice due to a retry upstream, it will post a duplicate Slack message. Implement a deduplication check using the reporting week's ISO date string as a unique key stored in the error/audit log table. If a record for that week already exists with status 'success', skip the post.
  • Confirmed before build: Slack App created and installed; bot token issued; channel ID confirmed; Looker Studio dashboard URL confirmed and access settings verified; stakeholder Slack user IDs confirmed.

03End-to-end data flow

Full end-to-end data flow: trigger through all three agent handoffs to Slack delivery. Field names match the agent IO contracts in section 02.
// =====================================================================
// BUSINESS KPI DASHBOARD  |  END-TO-END DATA FLOW
// =====================================================================

// TRIGGER
// Source: Time-based schedule OR manual webhook
// Field:  trigger_type = 'schedule' | 'webhook'
// Field:  reporting_period_start = '2025-01-13T00:00:00Z'  (auto-calc)
// Field:  reporting_period_end   = '2025-01-19T23:59:59Z'  (auto-calc)
// Field:  run_id = uuid_v4()                               (generated)

// =====================================================================
// AGENT 1: DATA COLLECTION AGENT
// =====================================================================

// --- Xero API call ---
// Endpoint: GET https://api.xero.com/api.xro/2.0/Reports/ProfitAndLoss
// Params:   fromDate=reporting_period_start, toDate=reporting_period_end
// Auth:     OAuth 2.0 Bearer token (scope: accounting.reports.read)
xero.total_revenue        = Reports[0].Rows[revenue_row].Cells[1].Value
xero.outstanding_invoices = Invoices.filter(Status='AUTHORISED').length
xero.outstanding_value    = Invoices.filter(Status='AUTHORISED').sum(AmountDue)
xero.cash_position        = BankAccounts[primary_account_id].CurrentBalance

// --- HubSpot API call ---
// Endpoint: POST https://api.hubapi.com/crm/v3/objects/deals/search
// Filter:   closedate BETWEEN reporting_period_start AND reporting_period_end
// Auth:     Private App Bearer token (scope: crm.objects.deals.read)
hubspot.deal_count        = response.total
hubspot.pipeline_value    = response.results.sum(properties.amount)
hubspot.conversion_rate   = closed_won_deals / total_deals_in_period

// --- Anomaly gate (inline, before Sheets write) ---
// For each KPI field: if abs((current - prior) / prior) > threshold_pct
//   -> set anomaly_flag = true
//   -> append field name to anomaly_fields[]
//   -> send Slack DM alert to ops manager
//   -> await webhook approval (timeout 4h -> halt, log error)
// If no anomalies: continue immediately

// --- Google Sheets write ---
// Endpoint: PUT https://sheets.googleapis.com/v4/spreadsheets/{SHEET_ID}/values/{RANGE}
// Auth:     Service account JSON key (role: Editor on sheet)
// Tab:      KPI_Input
sheets.cell_B2            = xero.total_revenue
sheets.cell_B3            = xero.outstanding_invoices
sheets.cell_B4            = xero.outstanding_value
sheets.cell_B5            = xero.cash_position
sheets.cell_B6            = hubspot.deal_count
sheets.cell_B7            = hubspot.pipeline_value
sheets.cell_B8            = hubspot.conversion_rate
sheets.cell_B1            = run_id                  // audit cell
sheets.write_status       = 'success' | 'partial' | 'failed'

// --- Agent 1 emits ---
agent_1.status            = 'complete'
agent_1.cells_written     = integer
agent_1.timestamp         = ISO8601 datetime

// =====================================================================
// HANDOFF: DATA COLLECTION AGENT -> KPI COMMENTARY AGENT
// Condition: agent_1.status == 'complete'
// Payload passed: run_id, reporting_period_start, reporting_period_end,
//   sheets.write_status, anomaly_flag, anomaly_fields, ops_manager_approved
// =====================================================================

// =====================================================================
// AGENT 2: KPI COMMENTARY AGENT
// =====================================================================

// --- Google Sheets read (current period) ---
// Tab: KPI_Input, range: B2:B8
current.total_revenue        = sheets.cell_B2
current.outstanding_invoices = sheets.cell_B3
current.outstanding_value    = sheets.cell_B4
current.cash_position        = sheets.cell_B5
current.deal_count           = sheets.cell_B6
current.pipeline_value       = sheets.cell_B7
current.conversion_rate      = sheets.cell_B8

// --- Google Sheets read (prior period, history tab) ---
// Tab: KPI_History, last populated row
prior.total_revenue          = KPI_History[last_row].col_B
prior.pipeline_value         = KPI_History[last_row].col_G
prior.conversion_rate        = KPI_History[last_row].col_H

// --- Baseline check ---
// If KPI_History has zero rows: set is_baseline_week = true
// Commentary prompt instructs model to note this is week 1 baseline

// --- LLM inference ---
// Endpoint: POST https://api.openai.com/v1/chat/completions
// Model:    gpt-4o
// Auth:     Bearer API key from credential store
prompt.kpi_delta_summary     = JSON.stringify(delta_object)   // computed deltas
prompt.tone_instruction      = 'concise, factual, plain English, under 120 words'
prompt.format_instruction    = '3 to 5 bullet points; no markdown headers'
commentary.text              = response.choices[0].message.content

// --- Notion write ---
// Endpoint: POST https://api.notion.com/v1/pages
// Auth:     Integration token (Bearer), invited to target database
// Page properties written:
notion.page_title            = 'KPI Report: Week ending ' + reporting_period_end.date
notion.date_property         = reporting_period_end
notion.body_block            = commentary.text
notion.page_id               = response.id
notion.page_url              = response.url
notion.write_status          = 'success' | 'failed'

// --- Append to KPI_History tab ---
// Tab: KPI_History, append new row with current period values
KPI_History.append_row([reporting_period_end, total_revenue, ..., run_id])

// --- Agent 2 emits ---
agent_2.status               = 'complete'
agent_2.notion_page_url      = notion.page_url
agent_2.commentary_text      = commentary.text

// =====================================================================
// HANDOFF: KPI COMMENTARY AGENT -> DISTRIBUTION AGENT
// Condition: agent_2.status == 'complete' AND notion.write_status == 'success'
// Payload passed: run_id, reporting_period_end, notion.page_url,
//   commentary.text, current.total_revenue, current.pipeline_value,
//   current.conversion_rate
// =====================================================================

// =====================================================================
// AGENT 3: DISTRIBUTION AGENT
// =====================================================================

// --- Deduplication check ---
// Query error/audit log table: WHERE week_key = reporting_period_end.date
//   AND post_status = 'success'
// If record found: skip Slack post, log duplicate attempt, halt

// --- Slack Block Kit message construction ---
message.headline             = 'KPI Report: Week ending ' + reporting_period_end.date
message.kpi_summary          = format([total_revenue, pipeline_value, conversion_rate])
message.commentary_block     = commentary.text.slice(0, 500)  // truncate if needed
message.dashboard_link       = looker_studio.dashboard_url    // static config value
message.notion_link          = notion.page_url

// --- Slack API call ---
// Endpoint: POST https://slack.com/api/chat.postMessage
// Auth:     Bot token xoxb-... (scope: chat:write)
// Params:   channel = slack.channel_id (config)
//           blocks  = Block Kit JSON payload
//           text    = fallback plain text
slack.message_ts             = response.ts
slack.post_status            = 'success' | 'failed'

// --- Audit log write ---
// Write final run record to Supabase audit table
audit_log.run_id             = run_id
audit_log.week_key           = reporting_period_end.date
audit_log.post_status        = slack.post_status
audit_log.completed_at       = ISO8601 datetime

// --- Agent 3 emits ---
agent_3.status               = 'complete'
// END OF FLOW
Developer Handover PackPage 3 of 4
FS-DOC-04Technical

04Recommended build stack

Orchestration layer
A workflow automation platform (tool TBC at project kickoff). Implement one discrete workflow per agent: 'workflow_data_collection', 'workflow_kpi_commentary', and 'workflow_distribution'. All three workflows share a single credential store so API keys and tokens are managed centrally and not duplicated across workflows. Credentials are referenced by name (e.g. 'xero_oauth', 'hubspot_private_app', 'google_service_account', 'notion_integration', 'slack_bot', 'openai_api_key') and never hardcoded.
Webhook configuration
Two inbound webhooks required. (1) Manual trigger endpoint: accepts a POST request from the ops manager (authenticated via a secret header token) to initiate an ad-hoc reporting run outside the schedule. (2) Anomaly approval endpoint: accepts a POST response from the ops manager (via a Slack interactive button or direct HTTP call) to approve or reject a flagged anomaly before the commentary agent proceeds. Both webhook URLs must be recorded in the project credential store and shared with the ops manager in the SOP. Outbound webhook calls to Slack use the standard Web API; no additional webhook config is needed on the Slack side beyond the bot token.
Templating approach
All dynamic text outputs (Slack messages, Notion page titles, LLM prompts) are constructed from versioned string templates stored as configuration variables within the orchestration layer, not hardcoded in workflow logic. The LLM system prompt and user prompt templates are stored as named config values ('prompt_system_kpi', 'prompt_user_kpi') so they can be updated without touching workflow logic. Slack Block Kit JSON is templated with variable interpolation for the headline, KPI figures, and URLs. Notion page structure (title property, date property, body block type) is defined once in the workflow config and applied consistently every cycle.
Error logging
All agent errors, anomaly flags, duplicate detections, and audit records are written to a Supabase table named 'kpi_dashboard_runs'. Schema: run_id (uuid), week_key (date), agent_name (text), status (text), error_detail (text, nullable), anomaly_fields (json, nullable), cells_written (integer, nullable), notion_page_url (text, nullable), slack_message_ts (text, nullable), created_at (timestamptz). On any error status write, the orchestration layer also sends a Slack DM to the ops manager from the bot account with a plain-text error summary including the run_id for traceability. The Supabase project must be provisioned and the table created before build begins.
Testing approach
All three agents are built and tested against sandbox or staging credentials first. Xero provides a Demo Company environment for API testing without affecting live accounts. HubSpot provides a developer sandbox account. A duplicate Google Sheet (named 'KPI_Input_STAGING') is used for write testing. Notion test pages are created in a dedicated 'Automation Testing' database. Slack test posts are sent to a private '#automation-testing' channel. Once each agent passes unit-level testing in staging, a full end-to-end integration test is run using staging credentials for two consecutive simulated reporting cycles. Only after both cycles pass does the build switch to production credentials. The two live production cycles required for go-live sign-off are documented in the Test and QA Plan.
Estimated total build time
Data Collection Agent: 10 hours. KPI Commentary Agent: 9 hours. Distribution Agent: 5 hours. End-to-end integration testing and staging cycles: 4 hours. Total: 28 hours. This matches the confirmed build_effort_hours in the project snapshot. Timeline across 4 delivery weeks includes discovery and access setup (week 1), Data Collection Agent build (week 2), KPI Commentary Agent build (week 3), and Distribution Agent build plus full end-to-end test cycles (week 4).
Critical pre-build dependencies: Xero OAuth app approved; HubSpot Private App token issued with correct scopes; Google Sheets service account created and granted Editor access to the master sheet; Notion integration invited to the target database; Slack App installed to workspace with chat:write scope and bot invited to the target channel; Supabase project provisioned with kpi_dashboard_runs table created; Looker Studio dashboard URL confirmed with correct sharing settings; anomaly threshold values per KPI agreed in writing with the ops manager. Build cannot begin on any agent until its credentials are in the shared credential store. Contact support@gofullspec.com with any access provisioning questions.
Developer Handover PackPage 4 of 4

More documents for this process

Every document generated for Business KPI Dashboard.

Launch Plan
Operations · Owner
View
ROI and Business Case
Finance · Owner
View
Process Runbook / SOP
Operations · Owner
View
Integration and API Spec
Technical · Developer
View
Test and QA Plan
Quality · Developer
View