Git Commit-to-Invoice Feature Specification¶
Overview¶
The Git Commit-to-Invoice feature enables IT freelancers and development teams to automatically transform code commits into billable invoice line items. By integrating with Git providers and project management tools (Jira, Linear, etc.), the system extracts contextual work data and generates draft invoice items for manual review and approval.
Core Philosophy¶
- Human-in-the-loop: All commits become draft line items requiring explicit user approval before invoicing
- Context-rich: Integrates with project management tools to provide meaningful descriptions for clients
- No double-billing protection via persistence: Rejected or ignored commits are tracked in the database to prevent recurring suggestions
- Flexible time tracking: Supports explicit time logging, heuristic estimation, or hybrid approaches
User Workflow¶
1. Initial Setup¶
User connects Git repository (GitHub/GitLab/Bitbucket)
↓
OAuth authentication with selected provider
↓
Webhook automatically configured for push events
↓
Optional: Connect Jira/Linear for ticket context enrichment
2. Commit Processing Pipeline¶
Git Push Event
↓
Cloudflare Worker receives webhook
↓
Commit metadata extracted (hash, author, message, timestamp, diff stats)
↓
Check: Commit already in database? → Skip if exists
↓
Parse ticket references (e.g., ABC-123) from commit message
↓
Fetch Jira/PM tool context (summary, epic, labels)
↓
Generate human-readable description
↓
Calculate time estimate (explicit, heuristic, or ML-based)
↓
Create DRAFT line item in Commit Inbox
↓
Notify user (in-app, email, or Slack)
3. User Review & Approval¶
Commit Inbox Interface:
- List view of all unprocessed commits grouped by:
- Ticket/Epic
- Date range
- Repository
- Project
Per Line Item Actions:
- Approve: Add to existing or new invoice draft
- Edit: Modify time, rate, description before adding
- Split: Divide across multiple invoice items or periods
- Ignore: Mark as non-billable (persisted to prevent re-suggestion)
- Defer: Keep in inbox for later decision
IT Ticketing System Integrations¶
The 5 Most Popular IT Ticketing Systems for IT Freelancers and Small IT Teams in the USA¶
| Rank | System | Key Features | Price Starting At |
|---|---|---|---|
| 1 | Zendesk | Market leader, highly scalable, extensive features | ~$19/agent/month |
| 2 | Freshdesk | Very popular with small teams, free plan available, good balance | ~$15/agent/month (free tier available) |
| 3 | Jira Service Management | IT teams & DevOps, deeply integrated in the Atlassian ecosystem | Free up to 3 agents, then ~$21/agent/month |
| 4 | Zoho Desk | Very affordable, good integration with Zoho ecosystem | ~$7-14/agent/month (free up to 3 agents) |
| 5 | Help Scout | Minimalist, simple to use, focused on email support | ~$25/agent/month |
Integration Benefits¶
Connecting a ticketing system enriches commit data with business context:
- Ticket summaries provide client-friendly descriptions
- Epic/Story hierarchy enables logical grouping for invoices
- Status tracking ensures only completed work is billed
- Time tracking sync from tickets can validate estimated hours
- Labels/categories help identify billable vs. non-billable work automatically
Data Model¶
Commit Entity¶
interface GitCommit {
id: string; // Internal UUID
hash: string; // Git SHA (partial or full)
shortHash: string; // First 7 characters
message: string; // Full commit message
authorName: string;
authorEmail: string;
committedAt: DateTime;
// Repository context
repositoryId: string;
branch: string;
// Diff statistics
filesChanged: number;
additions: number;
deletions: number;
// Processing status
status: "pending" | "drafted" | "invoiced" | "ignored";
// Timestamps
createdAt: DateTime;
updatedAt: DateTime;
ignoredAt?: DateTime; // When marked as ignored
invoicedAt?: DateTime; // When added to invoice
// Relations
projectId: string; // Links to Meister Bill project
ticketRef?: string; // e.g., "ABC-123"
draftItems: DraftLineItem[];
}
Draft Line Item Entity¶
interface DraftLineItem {
id: string;
// Source tracking
sourceType: "git" | "git+jira" | "git+linear" | "manual";
commitIds: string[]; // Can aggregate multiple commits
// Content
description: string; // Human-readable work description
detailedDescription?: string; // Technical details for internal use
// Time & Pricing
hours: number; // Billable hours
hourlyRate: number; // From project configuration
total: number; // Calculated: hours × rate
// Date range (for period-based billing)
startDate: Date;
endDate: Date;
// Categorization
category?: string; // Epic, component, or custom tag
ticketKey?: string; // Jira/PM tool reference
ticketUrl?: string;
// Status workflow
status: "draft" | "approved" | "invoiced" | "rejected";
// Relations
projectId: string;
invoiceId?: string; // Populated when added to invoice
// Timestamps
createdAt: DateTime;
approvedAt?: DateTime;
}
Project Configuration¶
interface ProjectGitConfig {
projectId: string;
// Git provider settings
provider: "github" | "gitlab" | "bitbucket";
repositoryUrl: string;
webhookSecret: string;
// Time estimation strategy
timeEstimation: {
strategy: "explicit" | "heuristic" | "hybrid";
explicitPattern?: string; // Regex to extract time from commit msg
heuristicFactor?: number; // Minutes per LOC changed
defaultHours?: number; // Fallback for unparseable commits
};
// Hourly rate (fixed per project)
hourlyRate: number;
currency: string;
// Integration settings
jiraConfig?: {
instanceUrl: string;
projectKey: string;
authToken: string;
};
// Filtering rules
excludePatterns: string[]; // e.g., ["^docs:", "^test:"]
includeOnlyTickets: boolean; // Only commits with ticket refs
billableLabels: string[]; // Jira labels that indicate billability
}
Integration Architecture¶
1. Git Provider Webhooks¶
GitHub:
POST /api/webhooks/github
X-GitHub-Event: push
X-Hub-Signature-256: sha256=...
GitLab:
POST /api/webhooks/gitlab
X-Gitlab-Event: Push Hook
X-Gitlab-Token: ...
Bitbucket:
POST /api/webhooks/bitbucket
X-Event-Key: repo:push
2. Jira Integration (Optional)¶
Authentication: OAuth 2.0 (3LO) for Jira Cloud, PAT for Server
API Usage:
// Fetch issue details
GET /rest/api/3/issue/{issueKey}
// Response enrichment
{
"key": "ABC-123",
"fields": {
"summary": "Fix authentication bug",
"issuetype": { "name": "Bug" },
"labels": ["billable", "backend"],
"customfield_10014": "EPIC-456",
"status": { "name": "Done" }
}
}
Rate Limiting: 10 requests/second with exponential backoff
3. Alternative PM Tools¶
| Tool | Auth | API Endpoint |
|---|---|---|
| Linear | Personal API Key | https://api.linear.app/graphql |
| Asana | OAuth 2.0 | https://app.asana.com/api/1.0/tasks/{gid} |
| ClickUp | Personal Token | https://api.clickup.com/api/v2/task/{task_id} |
| GitHub Issues | GitHub App | https://api.github.com/repos/{owner}/{repo}/issues/{number} |
Description Generation Strategies¶
Strategy 1: Simple (No AI)¶
Input: Commit message + file statistics Output:
"Backend Development: Fixed authentication bug
(2 files changed, +45/-12 lines)"
Implementation: Template-based with pattern matching
Strategy 2: Diff Summary (AI-Powered)¶
Input: Commit diff (truncated to 500 LOC) + commit message + Jira context Prompt:
Summarize the following code changes for a non-technical client invoice.
Focus on business value, not technical implementation details.
Be concise (max 20 words).
Commit: "ABC-123: Fix auth bug"
Jira: "User Authentication Fix - Critical bug allowing unauthorized access"
Changes: auth/middleware.ts (+45/-12 lines)
Output:
"Fixed critical authentication vulnerability in user login system"
Model: Claude 3 Haiku or GPT-4-mini (cost-optimized)
Strategy 3: Aggregated Sprint Summary¶
Use Case: Weekly/monthly invoicing with rolled-up commits
Input: All commits per Epic/Ticket over date range Output:
"API Modernization Sprint (Jan 15-31):
- Implemented OAuth2 authentication flow
- Migrated 3 legacy endpoints to REST
- Added comprehensive error handling
Total: 24 hours @ $150/hr = $3,600"
Time Estimation Methods¶
Method A: Explicit Time Logging¶
Commit Message Convention:
ABC-123 [2.5h]: Refactored user service
Pattern: \[(\d+\.?\d*)h?\] or configurable regex
Method B: Heuristic Estimation¶
Formula:
baseTime = 15 minutes // Minimum per commit
fileTime = filesChanged × 5 minutes
lineTime = (additions + deletions) × 0.5 minutes
total = baseTime + fileTime + lineTime (capped at 8 hours)
Calibration: User can adjust factor per project after initial data
Method C: Hybrid (Recommended)¶
- Parse explicit time from commit message
- If missing, apply heuristic
- If heuristic seems wrong (e.g., 1000 LOC change = 8+ hours), flag for review
Technical Implementation¶
Database Schema (Supabase)¶
-- Commits table
CREATE TABLE commits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
hash VARCHAR(40) UNIQUE NOT NULL,
short_hash VARCHAR(7) NOT NULL,
message TEXT NOT NULL,
author_name VARCHAR(255),
author_email VARCHAR(255),
committed_at TIMESTAMPTZ NOT NULL,
repository_id UUID REFERENCES repositories(id),
branch VARCHAR(255),
files_changed INTEGER DEFAULT 0,
additions INTEGER DEFAULT 0,
deletions INTEGER DEFAULT 0,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'drafted', 'invoiced', 'ignored')),
project_id UUID REFERENCES projects(id) NOT NULL,
ticket_ref VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
ignored_at TIMESTAMPTZ,
invoiced_at TIMESTAMPTZ
);
-- Draft line items
CREATE TABLE draft_line_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_type VARCHAR(20) NOT NULL,
description TEXT NOT NULL,
detailed_description TEXT,
hours DECIMAL(5,2) NOT NULL,
hourly_rate DECIMAL(10,2) NOT NULL,
total DECIMAL(10,2) GENERATED ALWAYS AS (hours * hourly_rate) STORED,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
category VARCHAR(100),
ticket_key VARCHAR(50),
ticket_url TEXT,
status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'approved', 'invoiced', 'rejected')),
project_id UUID REFERENCES projects(id) NOT NULL,
invoice_id UUID REFERENCES invoices(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
approved_at TIMESTAMPTZ
);
-- Junction table: commits to draft items (many-to-many)
CREATE TABLE commit_draft_items (
commit_id UUID REFERENCES commits(id) ON DELETE CASCADE,
draft_item_id UUID REFERENCES draft_line_items(id) ON DELETE CASCADE,
PRIMARY KEY (commit_id, draft_item_id)
);
-- Indexes for performance
CREATE INDEX idx_commits_project_status ON commits(project_id, status);
CREATE INDEX idx_commits_hash ON commits(hash);
CREATE INDEX idx_commits_committed_at ON commits(committed_at);
CREATE INDEX idx_draft_items_project_status ON draft_line_items(project_id, status);
Cloudflare Worker (Webhook Handler)¶
// src/workers/git-webhook.ts
export interface Env {
SUPABASE_URL: string;
SUPABASE_SERVICE_KEY: string;
WEBHOOK_SECRET_GITHUB: string;
WEBHOOK_SECRET_GITLAB: string;
}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext,
): Promise<Response> {
const url = new URL(request.url);
const provider = url.pathname.split("/").pop();
// 1. Verify webhook signature
const isValid = await verifyWebhook(request, provider, env);
if (!isValid) return new Response("Unauthorized", { status: 401 });
// 2. Parse payload
const payload = await request.json();
const commits = extractCommits(payload, provider);
// 3. Queue for processing (async)
ctx.waitUntil(processCommits(commits, env));
return new Response("Accepted", { status: 202 });
},
};
async function processCommits(commits: CommitPayload[], env: Env) {
const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_SERVICE_KEY);
for (const commit of commits) {
// Idempotency check
const { data: existing } = await supabase
.from("commits")
.select("id")
.eq("hash", commit.hash)
.single();
if (existing) continue;
// Fetch project configuration
const { data: project } = await supabase
.from("projects")
.select("*, git_config(*)")
.eq("repository_id", commit.repositoryId)
.single();
if (!project || !project.git_config) continue;
// Apply exclusion filters
if (shouldExclude(commit, project.git_config)) continue;
// Enrich with Jira if configured
let ticketContext = null;
if (project.git_config.jira_config && commit.ticketRef) {
ticketContext = await fetchJiraContext(
commit.ticketRef,
project.git_config.jira_config,
);
}
// Generate description
const description = await generateDescription(
commit,
ticketContext,
project.git_config.description_strategy,
);
// Calculate time
const hours = calculateHours(commit, project.git_config.time_estimation);
// Insert commit
const { data: commitRecord } = await supabase
.from("commits")
.insert({
hash: commit.hash,
short_hash: commit.shortHash,
message: commit.message,
author_name: commit.authorName,
author_email: commit.authorEmail,
committed_at: commit.timestamp,
repository_id: commit.repositoryId,
branch: commit.branch,
files_changed: commit.filesChanged,
additions: commit.additions,
deletions: commit.deletions,
status: "pending",
project_id: project.id,
ticket_ref: commit.ticketRef,
})
.select()
.single();
// Create draft line item
await supabase.from("draft_line_items").insert({
source_type: ticketContext ? "git+jira" : "git",
description: description,
detailed_description: commit.message,
hours: hours,
hourly_rate: project.git_config.hourly_rate,
start_date: commit.timestamp,
end_date: commit.timestamp,
category: ticketContext?.epic || commit.branch,
ticket_key: ticketContext?.key,
ticket_url: ticketContext?.url,
status: "draft",
project_id: project.id,
});
// Notify user
await notifyUser(project.user_id, "new_commits_pending");
}
}
Frontend: Commit Inbox Component¶
<!-- components/CommitInbox.vue -->
<template>
<div class="commit-inbox">
<div class="filters">
<select v-model="groupBy">
<option value="ticket">Group by Ticket</option>
<option value="date">Group by Date</option>
<option value="epic">Group by Epic</option>
</select>
<label>
<input v-model="showIgnored" type="checkbox" />
Show ignored commits
</label>
</div>
<div v-for="group in groupedCommits" :key="group.key" class="commit-group">
<h3>{{ group.title }} ({{ group.totalHours }}h)</h3>
<div v-for="item in group.items" :key="item.id" class="draft-item">
<div class="item-header">
<input
v-model="item.selected"
type="checkbox"
@change="updateSelection"
/>
<span class="ticket-badge">{{ item.ticketKey }}</span>
<span class="date">{{ formatDate(item.startDate) }}</span>
</div>
<div class="item-content">
<input
v-model="item.description"
class="description-input"
placeholder="Work description..."
/>
<div class="meta">
<input
v-model.number="item.hours"
type="number"
step="0.25"
class="hours-input"
/>
<span>@</span>
<span class="rate">{{ item.hourlyRate }}/hr</span>
<span class="total">= ${{ item.total }}</span>
</div>
</div>
<div class="item-actions">
<button @click="addToInvoice(item)">Add to Invoice</button>
<button @click="ignoreCommit(item)">Ignore</button>
<button @click="showDetails(item)">Details</button>
</div>
</div>
</div>
<div class="bulk-actions" v-if="selectedItems.length > 0">
<button @click="bulkAddToInvoice">
Add {{ selectedItems.length }} to Invoice
</button>
<button @click="bulkIgnore">Ignore Selected</button>
</div>
</div>
</template>
Security Considerations¶
Webhook Security¶
- HMAC signature verification for all providers
- IP allowlisting where supported (GitHub, GitLab)
- Webhook secrets rotated per project
Data Privacy¶
- Commit messages may contain sensitive info → sanitize before AI processing
- Jira tokens encrypted at rest (AES-256)
- Diff content processed in-memory only, never persisted (unless user opts in)
Access Control¶
- Users can only access commits for their own projects
- Row Level Security (RLS) policies on all tables
- Repository access verified via Git provider API on setup
Pricing & Packaging¶
Recommended Tiers¶
| Plan | Git Integration | AI Descriptions | PM Tool Connect |
|---|---|---|---|
| Starter | Basic (last 30 days) | ❌ | ❌ |
| Professional | Unlimited history | ✅ Standard | 1 connection |
| Agency | Unlimited + team features | ✅ Advanced | Unlimited |
Add-on Option¶
- Git Integration: $9/month (if not included in base plan)
- AI Description Upgrade: $5/month (better models, longer context)
Future Enhancements¶
Phase 2: Advanced Features¶
- Sprint-based invoicing: Auto-aggregate by sprint/iteration
- Team allocation: Split commits by contributor for team billing
- Client portal: Read-only view of work logs for transparency
- GitLab CI/CD integration: Track deployment time as billable hours
Phase 3: Intelligence¶
- ML time estimation: Train on user's historical data
- Anomaly detection: Flag unusual commit patterns (potential errors)
- Smart grouping: AI-suggested invoice groupings by logical work units
Success Metrics¶
- Adoption: % of projects with Git integration enabled
- Efficiency: Average time from commit to invoiced (target: < 5 minutes)
- Accuracy: User edit rate on AI-generated descriptions (target: < 20%)
- Revenue: Additional MRR from Git integration add-on
Open Questions¶
- Should we support pre-commit hooks to enforce time logging conventions?
- How to handle force-pushes with rewritten history (new commit hashes)?
- Should ignored commits be permanently hidden or recoverable?
- Do we need Git blame analysis to handle pair programming scenarios?