← All case studies

Case study · 08 / 11

Currency disposal, on-premises, with an evidence trail

A case study — Scrap Requests for a central bank


Summary

| | |
|---|---|
| **Problem** | A central bank's currency scrap-disposal process — request, approve, verify payment, execute — running on paper and email, with no reliable audit record. |
| **Constraint** | On-premises deployment is a regulatory requirement, not a preference. No SharePoint Online, no cloud services, no Power Automate. |
| **Solution** | A SharePoint full-trust farm solution in C# — a Visual Web Part over seven provisioned lists, with three-stage task-based approvals and field-level audit and email evidence logs. |
| **Outcome** | Built and packaged as a signed WSP farm solution. Bilingual with RTL, configuration-driven validation, complete queryable audit trail. Deployment is not evidenced in the repository — see §5. |
| **Role** | Full implementation — web part, code-behind, workflow, provisioning scripts, localisation, deployment. |
| **Stack** | C# · SharePoint Server-Side Object Model · Visual Web Part (ASCX + code-behind) · Feature/WSP packaging · PowerShell · User Profile Service · .resx localisation |

1. The problem

Damaged and withdrawn banknotes have to be taken out of circulation and destroyed, and in a central bank that process is heavily controlled. A department submits a request specifying how much of each denomination is being scrapped; it is approved; a payment is verified; the destruction is executed; the record is retained.

Running that on paper and email means the bank cannot answer the questions an auditor will ask: who approved this, when, what did it say before it was changed, and can you prove the notification went out.

2. The constraint that decided the platform

It had to run on-premises. In this sector that is a regulatory position rather than an IT preference, and it removes the entire modern SharePoint toolkit at a stroke: no SharePoint Online, no SPFx, no Power Automate, no Microsoft Graph, no cloud connectors.

What remains is the SharePoint Server-Side Object Model — a full-trust farm solution, deployed as a WSP, running in the SharePoint worker process with elevated privileges available. That is a 2010-era programming model, and it shapes everything: server-side page lifecycle, postbacks, ASCX user controls, and SPSecurity.RunWithElevatedPrivileges instead of an app identity.

The second constraint: the interface had to work in both English and Arabic, including right-to-left layout, because the bank's staff use both.

3. Architecture

A single Visual Web Part hosting the whole application, routed internally between create, edit and view modes, over seven provisioned lists:

| List | Holds |
|---|---|
| `Scrap_Requests` | The request header — requester, employee details, department, month key, status, totals, payment and execution state |
| `Scrap_Request_Lines` | One row per denomination per request |
| `Scrap_Approval_Tasks` | Approval, payment-verification and execution tasks |
| `Scrap_Audit_Log` | Field-level old/new value, who, when, why, change type |
| `Scrap_Email_Log` | Every notification attempt — recipients, subject, body, status, error |
| `Scrap_Email_Config` | Notification templates and recipients |
| `Scrap_Denomination_Config` | Per-denomination step and maximum-amount rules |

The audit and email logs are provisioned read-only to end users; only the service account, which runs elevated, has Contribute. An evidence trail that its subjects can edit is not evidence.

The request lifecycle runs through ten statuses — Draft, Submitted, UnderReview, ReturnedToModify, Modified, Approved, Rejected, Paid, Executed, Closed — with three task types dispatched to SharePoint groups at the appropriate stages.

4. The interesting decisions

4.1 Business rules in a list, not in code

Each denomination has its own constraints: how large a step the amount must be a multiple of, and what maximum is permitted. For example, QR 1 notes must be a multiple of 100 up to a maximum of 3,000; QR 10 notes a multiple of 1,000 up to 10,000; QR 50 a multiple of 50 with no ceiling.

These are rows in `Scrap_Denomination_Config`, seeded with defaults and editable by an administrator, not constants in the code-behind. A change to a threshold is a list edit, not a WSP redeploy — and in an on-premises farm, a redeploy means a change window, a farm solution retract and deploy, and an application pool recycle. Getting a rule out of the assembly is worth real money in operational cost.

The validation itself is layered: parse, check non-negative, then check each denomination against its configured multiple and maximum, with the rule text rendered back to the user in their own language so a rejection explains itself.

There is also a one-request-per-month rule, enforced server-side against a yyyy-MM month key stored on the request, so a department cannot submit twice for the same period regardless of what the browser allows.

4.2 A three-stage task workflow, hand-built

There is no Power Automate here, so the workflow is code. Three task types — Approval, PaymentVerification, Execution — each created against the members of a SharePoint group at the point the request reaches that stage.

Three details in the implementation are worth naming:

  • Tasks are completed on behalf of the real user while running elevated. The application needs elevation to write to lists the user cannot touch, but the *record* of who decided must be the actual person, not the service account. Losing that distinction destroys the audit value of the whole system.
  • Open tasks are closed automatically on state change. When a request moves on, tasks that are no longer relevant are completed rather than left dangling in someone's queue — the thing that makes hand-built workflows rot.
  • Assignment is by group, resolved at task-creation time, so staff changes are handled by group membership rather than by editing anything.

4.3 Two logs, because they answer different questions

The audit log is field-level. Every change writes the field name, the old value, the new value, who changed it, when, why, and a change type (Create, Update, StatusChange, SystemUpdate). Not "the request was modified" — *this field went from this to that.* That is what makes it answerable when an auditor asks what the request said before it was approved.

The email log records every send attempt, successful or not: recipients, recipient name, CC, subject, body, timestamp, status, and the error message on failure. A notification system that only records successes cannot distinguish "we told them and they ignored it" from "it never went out" — and in a regulated process those are very different findings.

4.4 Surviving schema drift across environments

Dev, test and production farms drift. Fields get created with slightly different internal names — an early manual creation here, a script run with a different version there — and code that hard-codes one internal name breaks on one environment and works on the others.

The code-behind uses a ResolveFieldName(list, params string[] candidates) helper: give it the names a field might have, and it returns the one that exists. That is not elegant, but it is the honest answer to a real operational condition, and it turns a class of deployment failure into a non-event.

4.5 Provisioning that is safe to re-run

The lists are created by PowerShell against the SSOM, with an Ensure-Field helper that adds a column only when it does not already exist, and an upsert pattern for seeding the denomination defaults. Running the script twice is a no-op rather than a duplication.

The scripts are also the documentation. Every column carries a comment explaining what it holds, so the provisioning script doubles as the data dictionary — which matters when the next person to touch this system may be several years away.

4.6 Localisation and identity

Strings come from .resx resources, with layout direction applied at runtime so the whole form mirrors in Arabic. Friendly field names are resolved per-language, so the audit log and validation messages read naturally in either.

Employee identity, department and employee ID are pre-populated from the SharePoint User Profile Service, so a requester is not retyping information the organisation already holds — and the values on the record come from the directory rather than from what someone typed.

5. Outcome

Built and packaged as a signed WSP farm solution. The application handles the full lifecycle from draft through three approval stages to execution and closure, in two languages, with a complete field-level audit trail and a queryable record of every notification attempt.

On whether it was deployed: the repository does not evidence it. There is no deployment log, no release note and no build output committed. The provisioning scripts are production-shaped — they carry a real farm hostname and seeded business rules — which is suggestive but not proof. The sibling solution built from this same codebase states in its own deployment guide that it has never been compiled. This write-up therefore claims what the source supports: the solution was built and packaged. Whether it ran in the bank's farm is a question for its author, not for the repository.

The single code-behind runs to roughly 4,200 lines. That is a consequence of the model: without a client-side framework or a workflow engine, routing, validation, audit diffing, task lifecycle, group resolution, email templating and attachment binding all live in one server-side control.

6. What I'd take from this

Platform constraints are sometimes the requirement. On-premises here is not technical debt; it is the compliance position. Treating it as something to work around rather than build for would have produced the wrong solution.

Configuration tables pay for themselves fastest where deployment is expensive. In a cloud solution, moving a business rule into a list saves a redeploy. In an on-premises farm it saves a change window, a retract-and-deploy and an app-pool recycle — so the calculation is not close.

An audit trail is only worth what its integrity is worth. Field-level diffs, elevated writes attributed to the real user, and logs its subjects cannot edit are three separate decisions, and skipping any one of them would have left something that looks like an audit trail without being one.

The old programming model still has to be held to modern standards. Idempotent provisioning, externalised configuration, resilient field resolution and localisation are not a function of the platform's age.