PAMAFix SAP project hit different. 💀
The Challenge
PAMA Persada had a monolithic ERP system that:
- Was slow as hell (startup time: coffee break ☕)
- One bug could crash everything
- Teams stepping on each other
- Deployments = pray no bugs 🙏
They decided: let's do microservices. so that's what we built.
What is OTC?
OTC = Order to Cash (sap jargon) it's the process from:
- Supplier delivers goods
- Create Berita Acara (delivery receipt)
- Invoice generation
- Payment tracking
- Reporting
simple right? except in mining industry where you have:
- Multiple suppliers
- Different pricing tiers
- Dynamic taxes
- Multiple currencies
- Audit requirements
- Integration with SAP (their main system)
yeah. not so simple. 😅
Tech Stack
Frontend:
- React with TypeScript (safety first)
- Ant Design (complex tables made somewhat manageable)
- Zod for validation (runtime type checking)
- React Query for server state
Backend:
- .NET microservices (not my choice but hey, enterprise loves .NET)
- Each domain = separate service
- API Gateway for routing
- PostgreSQL per service
Key Features I Built
1. Dynamic Form Handler
This was the hardest part. forms that change based on:
- User role
- Transaction type
- Supplier category
- Amount thresholds
interface FormConfig {
fields: FieldConfig[];
rules: ConditionalRule[];
validation: ZodSchema;
}
const DynamicForm: React.FC<{ config: FormConfig }> = ({ config }) => {
const { watch } = useFormContext();
return (
<>
{config.fields.map(field => {
if (isConditional(field, watch())) {
return <DynamicField key={field.name} {...field} />;
}
return null;
})}
</>
);
};
2. Complex Table Component
Opex reports with 18+ columns. users can:
- Show/hide columns
- Reorder columns
- Pin columns
- Export to Excel
- Drill down into sub-items
// expandable row example
const ExpandableRow = ({ record }: { record: OpexRecord }) => {
const [expanded, setExpanded] = useState(false);
return (
<>
<tr onClick={() => setExpanded(!expanded)}>
<td colSpan={18}>{record.summary}</td>
</tr>
{expanded && (
<tr>
<td colSpan={18}>
<DrillDownTable data={record.details} />
</td>
</tr>
)}
</>
);
};
3. Error Handling in Microservices
When you call 5 services to complete one operation, errors can happen anywhere:
async function createTransaction(data: TransactionInput) {
try {
// Call services in parallel where possible
const [supplier, pricing, tax] = await Promise.all([
supplierService.validate(data.supplierId),
pricingService.calculate(data.items),
taxService.getRates(data.category)
]);
const transaction = await transactionService.create({
...data,
supplier,
pricing,
tax
});
return transaction;
} catch (error) {
// Handle partial failures
if (error instanceof PartialFailure) {
// Rollback what we can
await transactionService.rollback(error.failedOps);
throw new UserFriendlyError(
`Some items failed: ${error.reasons.join(', ')}`
);
}
throw error;
}
}
4. Column Management
Users wanted control over their views:
const useColumnVisibility = (defaultColumns: Column[]) => {
const [visibleColumns, setVisibleColumns] = useState(
() => localStorage.getItem('opex_columns') ?? defaultColumns
);
const toggleColumn = (colId: string) => {
setVisibleColumns(prev =>
prev.includes(colId)
? prev.filter(id => id !== colId)
: [...prev, colId]
);
localStorage.setItem('opex_columns', JSON.stringify(visibleColumns));
};
return { visibleColumns, toggleColumn };
};
Collaboration with Backend
This was my first time working closely with .NET team. learned:
- API contracts matter more than ever - we used OpenAPI specs
- Error responses need standardization - everyone used different formats initially 💀
- Data consistency - eventual consistency in microservices is real
- Debugging distributed systems - correlation IDs saved us
Challenges
Dynamic forms are HARD every business rule seemed to add a new condition. eventually we built a rules engine. learned that building tools > building features.
Testing microservices how do you test something that depends on 5 other services? answer: test containers, mocks, and lots of patience.
Performance initial load was slow because we were fetching from multiple services. solved with:
- React Query caching
- Service-level caching
- Optimistic updates
Results
- ✅ Users can handle complex financial data entry efficiently
- ✅ 18+ column tables with full user control
- ✅ Resilient error handling
- ✅ Smooth .NET integration
- ✅ Improved performance from original monolith
What I Learned
- Microservices = organizational complexity - Conway's Law is real
- Frontend in microservices - you're only as good as your API contracts
- Dynamic forms - build a rules engine early
- .NET is actually fine - different doesn't mean bad
this project made me appreciate both frontend AND backend deeply. can't do good frontend without understanding the system it connects to.
Related Articles
PAMAFix Opex Capex - Financial Reporting Module
Built complex financial reporting with expandable tables, dynamic summaries, and Budget vs Actual comparisons.
Sinartama E-RUPS - Real-Time Electronic Shareholder Meetings
Built a real-time E-RUPS platform supporting 300+ concurrent users with Zoom integration, WebSocket voting, and multi-role access control.
Music Distribution Platform - Licensing & Digital Contracts
Built a music distribution platform with payment gateway, digital signatures, and 20GB file uploads.