Back to Case Studies

From Zoho and AWS Lambda to One Odoo Module

A membership business ran its subscriptions, its CRM sync and a 2.9 GB provider directory across seven Lambdas, two Deluge scripts and a pile of CSVs. I rebuilt it as one Odoo 17 module, ran it on a real Odoo instance against the client's own files, and found nine defects in the original on the way.

odoo 17zoho crmzoho subscriptionsaws lambdamigrationpython
Odoo subscription form with chatter and the provider directory grouped by state
Customer and provider names are sample data; the four figures are from the real test run.

What was running

The business sold dental membership plans. Payments and renewals lived in Zoho Subscriptions, customers in Zoho CRM, leads went to Mailchimp, a monthly eligibility file went to a network partner, and a public provider directory was rebuilt from three source files every month.

Holding it together: seven AWS Lambda functions, two Deluge scripts inside Zoho, and a set of Python scripts that produced roughly 2.9 GB of CSV across 328 files. Each piece was deployed on its own, logged to CloudWatch, and understood by whoever wrote it.

What I built

One Odoo 17 module, Community compatible, that carries all four jobs:

  • Subscription lifecycle. The five rules that used to live in Lambda are now methods on real records, with chatter on every state change. One controller replaces four separately deployed functions and keeps the same payload shapes, so Zoho's existing webhooks only needed a new URL. Two nightly crons catch anything a missed webhook would have dropped, which nothing did before.
  • Lead sync to Mailchimp. Odoo hashes the email itself, so the Lambda and API Gateway that existed only to compute an MD5 are gone. The sync is queued and retried by a cron instead of running inside the save, so a Mailchimp outage no longer blocks a salesperson, and the sync state and error text sit on the lead form.
  • The monthly eligibility file. The same 229-character fixed-width layout, generated straight from Odoo data, with no report export step and no auth token in code. The six hard-coded dependant columns became the member's actual contact children.
  • The provider directory. Providers became records instead of rows in a 400 MB CSV: searchable, groupable by state, network and fee schedule, editable by staff. The import wizard reads all four source formats, the export wizard reproduces the exact split rules, and the geocode cache moved from DynamoDB into the database that already gets backed up.

Nine defects the rewrite found

Porting code line by line is the most thorough code review there is. The ones that mattered:

Renewalsa typo in one key meant renewals silently stayed on manual collection, so that path never worked
Map pinslatitude was read into longitude, so every provider geocoded by that script landed in the wrong place
Open webhooksanyone who knew a URL could cancel a subscription; the Odoo endpoints require a shared secret
Silent failuresbare except and print meant a failed card deletion vanished; every call is now logged with its payload and outcome

Also: the Mailchimp create call used POST, which fails for anyone already on the list; the monthly dedup compared against one in-memory file, so last month's provider came back this month; and the bio templates passed more arguments than placeholders, so the network name never printed.

One more, which is not a code defect but the most urgent finding: live Zoho, AWS, Geocodio and Mailchimp keys were hard-coded in committed source. In the module they are Odoo system parameters. The keys themselves still need rotating, and I said so in writing.

What the tests actually showed

The module was installed on a real Odoo 17 instance with PostgreSQL 14 and run against the client's own files, not sample data. It installs clean on an empty database: ten models, the views, menus, security groups, access rules, five scheduled jobs and the system parameters all load without an error or a warning. Nine unit tests pass.

1,681providers imported from 2,000 real lines; 319 skipped as duplicate phone numbers
0created on a second import of the same file, so a repeated run cannot double count
38state CSV files exported, including the California split by fee schedule, header identical to the original
7 of 7webhook cases behaved: no secret and wrong secret rejected, a malformed payload answered 400 instead of being swallowed

Fixed-width parsing landed exactly, with no leftover characters on any line. Phone, zip, state, specialty and language expansion all came out right, fee schedules matched by zip prefix, and the 229-character eligibility file put every field in its correct column.

The run also surfaced a typo in the original export header: the third column is spelled excert, not excerpt. The Odoo export keeps that exact spelling, because the downstream WordPress import depends on it, and a test now pins the full 38-column header so it cannot drift.

What is left before go-live

  • Run the full 117 MB provider file, not the 2,000-line slice, and diff the exported state files against the current production output row by row.
  • Fill in the Mailchimp key, the Geocodio key and the webhook secret in system parameters.
  • Rotate the credentials that were exposed in the old source.
  • Point the Zoho webhooks at the new Odoo URLs.

I list those plainly rather than calling the job finished. A migration is done when the old and new outputs match on the full data set, not when the tests pass on a slice.

The earlier work on the same system

Three contracts with the same client between 2018 and 2020 built parts of what this module now replaces: a Zoho Subscriptions webhook updating Zoho CRM through AWS Lambda, and a repeatable merge that turned three separate provider files into one clean directory listing.

Moving off a Pile of Scripts?

Zoho, Lambda, cron jobs and CSVs that only one person understands. I port them into one system, and tell you what was broken on the way.