Amirali YaghoutiSenior Software Engineer

webapp Case study

Bale / Rubika / Eitaa Customer Messaging

Reaching customers in Iran means Bale, Rubika and Eitaa rather than the platforms most messaging tooling assumes. Each has its own API and its own failure behaviour, and none of them should be visible to the code that decides what to send.

The business problem

Messaging integrations rot when provider details leak into business logic. Add a second platform and you either duplicate the campaign code or thread conditionals through it. On top of that, customer messaging carries obligations that are not technical: opt-out has to be honoured permanently, OTP traffic must not share a path with campaign traffic, and someone has to approve a template before thousands of people receive it.

What I delivered

  • a2-bale-customer-hub, a WordPress plugin connecting customer records to Bale bot and Safir messaging workflows.
  • Provider-specific logic isolated behind platform and OTP provider classes, so adding or replacing a channel does not reach into campaign code.
  • Campaign queues with templates, so a send is a reviewable object rather than a loop that has already started.
  • Opt-out stored as a record rather than inferred, because an unsubscribe that depends on a query being written correctly is one bad query away from a compliance problem.
  • Contact import with integration into WooCommerce and CRM contact sources where they are available.
  • Support request handling on the same customer records, so an inbound question and an outbound campaign share a view of the customer.

Technical approach

  • The provider boundary is the architecture. Everything above it talks about customers, templates and queues; everything below it knows about one platform's API.
  • OTP is kept behind its own provider abstraction, separate from campaign messaging, because the two have different urgency, different failure handling and different consequences when they are confused.
  • Queues rather than direct sends, so a campaign can be inspected, paused and reasoned about instead of being a request that either finished or did not.
  • Opt-out is checked as a positive record at send time. Anything less makes correctness depend on every future query being written carefully.
  • Tokens, webhook secrets and contact exports are excluded from the repository by policy, and the public description of this work stays at architecture level for that reason.

Result and evidence

Customer messaging runs across the Iranian platforms the business actually needs, with the provider details contained and the customer-protection rules enforced structurally rather than by convention.

Commercial value

Messaging is where a commerce business damages its relationship with customers fastest. Building the review step and the opt-out record in from the start is much cheaper than retrofitting them after an incident.

implementation-brief.readme

Readable implementation brief

implementation_brief {
  project: "Bale / Rubika / Eitaa Customer Messaging"
  plugin: "a2-bale-customer-hub (WordPress, PHP 7.4+)"
  boundary: "platform + OTP provider classes; campaign code
             never sees a provider API"
  otp: "separate provider abstraction from campaign sends"
  queues: "campaigns are inspectable objects, not loops"
  optout: "stored record, checked at send; never inferred"
  sources: "WooCommerce + A2/Yaghout CRM contact import"
  excluded_from_repo: "tokens, webhook secrets, OTP secrets,
                       contact lists, exports, logs"
}

What this project shows

The adapter boundary is ordinary good practice; what I would point at is putting OTP behind a separate provider from campaigns. Mixing them is a common shortcut and it fails in the worst possible way.

Treating opt-out as stored state rather than a derived condition is the kind of decision that looks like overkill until the day it does not.