Case study · Desktop app to web app

A 2006 Delphi program, now a web app

We converted Virtual Salesman 3, an open-source Delphi 7 program for quotations, orders, invoices and inventory, into a web app on React, C# and PostgreSQL. We wrote its rules down in plain words, recovered its scrambled file format from the program itself so the old data moves over, and re-created its arithmetic: the author’s four sample documents match to the bit, and the new totals match to the cent. On the way we found a numbering bug that could overwrite a saved document, and fixed it. It took about an hour, start to finish.

Last updated

Converted Nov 1, 2026About 1 hour, start to finishDelphi 7 → React, C#, PostgreSQL

Virtual Salesman 3 is a Windows program for quotations, orders, invoices and inventory, written in Delphi by Marc-André Lamothe between 1998 and 2006 and published as open source in 2016. Small businesses ran on programs like it, and many still do. We moved it to the web without losing a rule or a record.

Open the web versionThe original

11,272 linesof Delphi in 35 units and 23 forms
1 file formatrecovered from the .exe, not the source
4 of 4sample documents match to the bit
24 testsdecoding, totals, rules and the database
The brief

Same program, in a browser

We wanted a real desktop program, not one we wrote to make a point: real rules, real data files, real quirks. Virtual Salesman was a good fit. It does the everyday work of a small business, it’s written in Delphi, a language few developers still use, and its source is public under the GPL-3, so we can show every step.

Challenge

Move every screen, rule and record to the web. The program’s data files are scrambled, and the code that reads them isn’t in the published source.

Approach

List the rules in plain words, recover the file format from the program itself, prove the math against the author’s own files, then build.

Result

A web app on our standard stack that reads the old files, matches the old totals, and fixes the bug that could overwrite saved documents.

Before and after. The old program kept everything in a folder of scrambled files on one PC; the web version keeps it in a database behind an API, and reads the old files in.
In the desktop programIn the web app
Forms and dialogs (23 .dfm files)React screens, same tabs in the same order
Rules inside button and field event handlersOne C# rules file, checked by the API on every save
One scrambled binary file per company, inventory and documentPostgreSQL tables with keys that refuse duplicates
String grids with keyboard shortcutsTables with the same shortcuts: Ctrl+Enter, Enter, Delete, Ctrl+F, Ctrl+S
Printing code (UnitPrint.pas, 995 lines)A print page: print it or save it as a PDF
users.dat, passwords in plain textUsers and the same 25 rights, checked on the server
Installed on each Windows PCOne server, any browser, phones included
Step 1

Read the code and write the rules down

On this conversion

The rules weren’t in one place. The totals were in UnitTransaction.pas, the deposit rules in the transaction form’s button handlers, the stock check in the product picker, and numbering and locked invoices in the main window’s Save and Delete handlers. Here they are in plain words:

  1. A document only moves forward: a quotation becomes an order or an invoice, an order becomes an invoice. The earlier one stays as it was.
  2. A quotation takes no deposit and no payment.
  3. An order or invoice needs a deposit of at least the company's percentage of the total.
  4. The Deposit button suggests that percentage, or the whole amount for an invoice that never was an order.
  5. State tax is charged on the price plus the federal tax, as Quebec did with GST and QST until 2013.
  6. A product's price is copied onto the line when it's added, and stays when the inventory price changes.
  7. A line can't ask for more than is in stock, or for nothing.
  8. Saving an invoice takes its quantities out of the inventory, after asking.
  9. Saved invoices can be locked: they can't be changed or deleted.
  10. Numbers of clients, employees, suppliers, groups and products are used once and never change.
On your app

Ask for this list before anything else. If the owner reads it and says “that’s not how we work,” you’ve saved a rewrite. Every rule becomes a test later.

Step 2

Get the data out

On this conversion

The program saves each company, inventory and document as a scrambled binary file. The unit that reads and writes them, EncryptedFile, isn’t in the published source, so the program can’t even be compiled from it. We found the format in the program itself: the machine code of VS3.exe, read with LLVM’s disassembler (objdump), showed the calls lining up with the source, and then the scrambling.

Each file starts with a 4-byte seed. Every value after it is shifted by a key: half the seed, plus the next entry of a 1,024-entry table built into the program. All 9 sample files in the repository decode with not one byte left over.

On your app
C# · reading the old files
// Every value in a .vs3, .v3i or .v3t file is a 4-byte number shifted by a key.
// The key: half the file's seed (rounded half to even, as Delphi's Round does)
// plus the next entry of a 1,024-entry table built into VS3.exe.
public uint ReadUInt()
{
    uint raw = BitConverter.ToUInt32(_data, _pos);
    _pos += 4;
    uint key = (uint)((_half + Table[_index]) & 0xFFFFFFFF);
    _index = (_index + 1) & 0x3FF;
    uint value = unchecked(raw - key);
    return value == 0xFFFFFFFF ? 0 : value;
}

// Text: a length, then one number per character (Windows-1252).
// Money and dates: the 32 bits of a Delphi Single.

Most old programs keep their data somewhere easier: an Access or Paradox database, a local SQL Server, dBase files. Find out where yours lives and how much of it there is. A file format nobody documented is still readable; it just takes longer.

Step 3

Prove the math with the old files

On this conversion

The old program stored money as 32-bit floating point numbers. We re-created its arithmetic, rounding included, and recomputed the four sample documents (a quotation, two orders and an invoice). All four give exactly the bits the program saved. Then the new arithmetic, in decimal, gives what the program printed, to the cent.

Same totals, proven on the owner’s own files.

On your app
C# · the original arithmetic, re-created
// TTransaction.Calculate, re-created to test against the saved files:
// values kept as 32-bit Singles, each step computed wider, then stored.
float sub = 0, taxable = 0;
foreach (var l in lines)
{
    if (l.Descriptive) continue;               // "text only" lines don't count
    double amount = l.Quantity * (double)l.Price;
    sub = (float)(sub + amount);
    if (l.Taxed) taxable = (float)(taxable + amount);
}
float fed = (float)(taxable * ((double)fedRate / 100));
float state = (float)((taxable + (double)fed) * ((double)stateRate / 100));
float total = (float)((double)sub + fed + state);

Pick documents the owner knows: last month’s invoices, a tricky order with a discount, one with a refund. If the new app gives the same totals on all of them, people trust it. If it doesn’t, you’ve found a rule nobody told you about.

Step 4

Decide what to fix, and what to keep

On this conversion

Reading every line turns up problems nobody knew about. The worst one here: the program could give two orders the same number, and the second would silently replace the first. The author’s own sample files show it: the company file says the last order was 108, while order 109 exists, so the next order would have been 109 too.

On your app
FoundWhat the old program didWhat we did
Document numbers could collideSave wrote the company file first, then the document, then raised the counter in memory without marking the company as changed. Close the program and the counter was lost: the next document got the same number and its file replaced the earlier one. The author’s own sample data is in that state.Fixed. The number comes from the database inside the same transaction, a unique key refuses a second one, and the importer repairs the counters.
Totals that didn’t add upMoney was 32-bit floating point, rounded only for display. In 100,000 random invoices, 23,423 printed a total a cent away from their own sub-total plus taxes.Changed, and flagged for the owner: each tax is rounded to the cent and the total is their sum. On 23,392 of those invoices the new total differs from the old one by a cent.
Whole card numbers in the filesThe credit card number and expiry were saved with every order and invoice.Fixed. Only the last four digits are kept, and the importer cuts old numbers down.
Passwords in plain textusers.dat held each user’s password as typed, scrambled the same way as the data.Replaced: rights are checked by the server for every change, and every change is logged with who made it.
Two deposit rules disagreeThe Deposit button suggests a percentage of the total with fees; the check on save uses the total without fees.Kept as it was, and written down for the owner to decide.
Stock can go below zeroStock is checked when a line is added, and taken out when the invoice is saved, so two orders can sell the same last item. Text-only lines leave stock too.Kept as it was, shown in red, and written down for the owner to decide.

Fix what loses data or exposes it. Keep what people rely on, even if it looks odd, until the owner decides. Write every decision down: that list is what you go through together before the switch.

Step 5

Build it on a standard stack

On this conversion

The web version runs on the same stack as this site: React and TypeScript screens, a C# API on ASP.NET Core and a PostgreSQL database. The rules live in the API: the screen sends a draft as you type and shows the totals and problems the server works out, so the browser is never trusted with a price or a tax.

It keeps what long-time users know: the five tabs in the same order, the same columns, and the same shortcuts. It adds what the old program never had: a log of every change and who made it, rights checked by the server, a print page that saves as PDF, and an import page for the old files.

On your app
  • Keep the screens familiar. People who have used a program for 15 years work by habit: tab order, shortcuts, where the totals sit.
  • Put every rule on the server, once. A rule only in the screen can be skipped; a rule in two places will drift.
  • Let the database refuse what must never happen, like a document number used twice.
  • Watch for what doesn’t move easily: scanners, scales and special printers, files on the C: drive, working offline, and third-party grid and report components. This program had none of them; many do.
Step 6

Test it like the old program

On this conversion

24 automated tests run on every deploy: the file decoding, the old and new arithmetic, the rules, and the whole workflow against a real PostgreSQL database. Then a browser went through it the way a person would: open the sample quotation, make it an invoice, check that the stock went down, try a deposit that’s too small, sign in as the sales clerk and try to change a price. It ran under this site’s security policy, at desktop and phone size.

On your app
Terminal · the test run
$ dotnet test
  CompatibilityTests   7 passed   every sample file decoded, nothing left over
                                  4 of 4 documents: totals match bit for bit
  RulesTests           9 passed   taxes, deposits, forward-only, 100,000 random invoices
  StoreTests           8 passed   the workflow against PostgreSQL
Passed!  Failed: 0, Passed: 24

The tests are also what lets you change things later. When the owner says “the deposit should include fees after all,” you change one rule and the tests tell you what else moved.

The clock

How long it took

Claude Code did the work on Nov 1, 2026, with Jan making the calls on the license and the scope. The times come from the sessions’ clocks and the files’ timestamps, in Pacific time. About an hour from start to finish, including the wait for answers; writing this page and Jan’s review of the code come on top.

WorkTimeWhat came out of it
Pick the program, record the start18:25–18:26Two open-source Delphi programs downloaded; Virtual Salesman chosen
Read the code, recover the file formatfrom 18:26Disassembly and a working decoder by 18:29; then the rules, and all 9 files checked
Questions for Januntil 19:05The license (a public GPL repository) and the scope (the core workflow)
C# API, database and 24 tests19:05–19:14The numbering bug confirmed in the code and in the data
React screens19:14–19:20Inventories, people, documents, printing, import, activity
Browser walkthrough and fixes19:20–19:25Two bugs found and fixed; the site’s security policy, phone size
Deploy scripts and README19:25–19:27Its own service, database and web address

This was a small program: one person’s work, with no hardware, no report designer and no third-party components. A larger one takes longer, and most of the extra time goes to steps 1 and 4: finding the rules and deciding about what you find.

Lessons

What we’d tell anyone moving an old program

  1. List the rules before writing code.Most of them lived in the forms’ event handlers, not in a business layer. A list in plain words is what the owner can check.
  2. Prove the numbers with the old data.Re-creating the old arithmetic, 32-bit rounding included, and matching what it saved is what makes “same behavior” a fact.
  3. Expect to find bugs, and decide each one out loud.Fix what loses data, keep what the owner relies on, and write down everything in between.
  4. The data matters as much as the screens.A web app nobody can move their files into is a demo. Here the importer is part of the product.
  5. Check the license first.The original is GPL-3, so the web version is too: it lives in its own repository and runs as its own program, apart from this site’s code.
Try it

Open it, break it, read it

The web version starts with the author’s own sample company. Your copy is yours alone and is deleted after 48 idle hours. Make a quotation, turn it into an invoice, switch to the sales clerk, or start over from the sample files. If you still have Virtual Salesman 3 files, import them.

Open the web versionThe original on GitHub

Virtual Salesman 3 is © 1998–2006 Marc-André Lamothe, published under the GNU General Public License version 3. The web version is a modified version under the same license.