MLM Admin Applet
Run a member-gets-member referral programme: a program with a start and end date, members enrolled as participants, and the lineage of who introduced whom, up to ten levels deep, filled by invitation or by a participant’s QR link. The applet records the tree and pays nobody — the money is an MLM-type scheme in Commission Scheme, and the commission run uses the cycle’s scheme, not the one named on the program. Take the scheme design to the company’s legal adviser before the first invitation is sent; nothing here distinguishes paying on sales from paying on recruitment.
Overview
The MLM Admin applet is where you run a member-gets-member referral programme. You create a program (a referral campaign with a start and end date), enrol members from Membership Admin as participants, and the applet keeps the lineage — who introduced whom, one upline-to-downline link at a time, up to ten levels deep. New downlines arrive by invitation (an e-mail, SMS or WhatsApp message sent through a contact-centre channel, accepted on a registration page) or by scanning a participant’s QR link, which drops them onto your storefront’s sign-up page with the inviter’s key attached.
The applet records the tree. It pays nobody: the money is an MLM-type scheme in Commission Scheme, and the Sales Commission posting run walks the lineage up from the buyer and writes one commission line per upline level.
mlm-admin-applet (TNT-USER, ACTIVE, created 2022-12-28; the registry’s documentation_url still points at a Confluence page). Master-data applet: no document, no journal, no stock. This page carries no captures — the repository ships none and static/images/ holds none.Where it fits
| Direction | Document / applet | How it connects |
|---|---|---|
| Upstream | Membership Admin | Every participant, inviter and downline is a membership card; the member’s own MLM tab shows the uplines and downlines this applet holds |
| Upstream | Customer Maintenance, Entity | Participants carry the member’s customer entity; entity contacts are named people under an entity who are not yet members |
| Upstream | Unified Contact Center | A program’s communication endpoint links name the e-mail / SMS / WhatsApp endpoints and message templates the invitation processor sends through |
| Upstream | Commission Scheme | A program may name a scheme; the commission run is driven by MLM-type schemes, not by that field (see Lifecycle) |
| Upstream | CP Commerce Admin | The program’s Website and URL Forward point at a storefront; a storefront sign-up carrying an inviter’s key creates the downline’s entity, membership, participant and lineage |
| Downstream | Sales Commission | The posting run reads the lineage for MLM schemes and writes one intermediate commission line per upline level; the commission report shows MLM Program and MLM Member columns |
Screens and menus
Route root: applets/wavelet/erp/mlm-admin-applet/. Opening the applet lands on Invitations.
| Menu item | Route | What it shows |
|---|---|---|
| Entity Contacts | contacts | People under a customer entity who can be invited before they are members: Name, Parent Entity, Email, Phone, Status; create and edit |
| Invitations | invitation | All invitations across programs: Inviter, Invitee, Program, Invite Method, Invite Status, Date Created, Expiry Date; + opens Invitation Create |
| Programs | program | Programs: Program Code, Program Name, Commission Scheme, Website, Start Date, End Date; + opens Program Create |
| Payout | generic | No such route exists — the entry opens the applet’s 404 page. A Commission Report entry above it is commented out |
Program Create has a single Details tab. Program Edit has five tabs and a Delete button:
| Tab | Content |
|---|---|
| Details | The program header (Commission Scheme is shown at create only; the select is commented out on edit) |
| Communication Endpoints | Listing of the contact-centre endpoints linked to the program: Virtual Contact, Endpoint Name, Endpoint ID, Priority, Channel Name. The listing reads correctly; create, update and delete on this tab are not wired (see Troubleshooting) |
| Participants | Participant, Object Type, Referral Source, Date Created, Date Effective; create picks a membership card on a tab the screen spells Partcipant (the misspelling is the product’s); edit has QR Code and Details tabs and Delete |
| Invitations | The same invitation screens as the top-level menu, filtered to the program; edit adds the Invitee field and Delete |
| Lineage | Upline, Downline, Level, Invite Method (the referral-source channel name), Invite Status, Date Created; create picks an upline and a downline from the program’s participants; edit allows Delete |
The participant’s QR Code tab renders the participant’s personal invitation link as a QR image, a copyable link and a PNG download — the self-service invitation link described under Lifecycle.
Settings and Personalization menus
| Entry | Route | Purpose |
|---|---|---|
| Field Settings | settings/field-settings | Eight slide-toggles copied from the document-applet template (Unit Discount, SST/VAT/GST, WHT, Blanket Order, Segment, G/L Dimension, Profit Center, Project); bound to nothing, Save has no handler |
| Default Selection | settings/default-selection | Default Branch and Default Location (see Configuration — the save path is broken) |
| Webhook, Feature Visibility, permission listings | settings/… | Shared platform screens; the settings menu’s default lands on Feature Visibility |
| Personalization → Default Selection | personalization/personal-default-selection | Per-user branch and location, same broken save path |
Configuration
Before you can use it
| Prerequisite | Where | Why |
|---|---|---|
| Members with a card number | Membership Admin | A participant is a membership card; the participant picker lists membership cards and nothing else (obj_type is always MEMBERSHIP in the UI) |
| Customer entities | Customer Maintenance | A participant is unique per program + entity + member; entity contacts hang under a parent entity |
| Contact-centre endpoints and message templates for e-mail, SMS or WhatsApp | Unified Contact Center | The invitation processor takes the channel from the program’s one ACTIVE endpoint link, and the sending endpoint and message template from its own scheduled-job settings (see Lifecycle); with no link, invitations stay CREATED |
A storefront website with post-registration configured, and a landing URL containing <URL-KEY> | CP Commerce Admin | The QR link redirects to the program’s URL Forward with the key substituted; the storefront sign-up links the new user to the inviter |
An MLM-type commission scheme and a commission cycle | Commission Scheme, Sales Commission | Without them the lineage earns nothing |
The two queue processors scheduled: MLM_INVITATION_LINK_PROCESSOR and INVITATION_INSERTION_INTO_LINEAGE_PROCESSOR | Scheduler | Invitations are sent and accepted invitations become lineage rows only when these run |
API permissions TNT_API_DM_MLM_{PROGRAM_HDR, PROGRAM_PARTICIPANT_HDR, ENTITY_CONTACT_HDR, LINEAGE_HDR, INVITATION_HDR, PROGRAM_CC_ENDPOINT_LINK}_{READ, CREATE, UPDATE, DELETE} | Settings → permission listings | Each listing, create and edit screen |
Applet settings
Settings live in an applet-local settings container (not the shared field-configuration component); there is no applet-settings model file in this project. Tenant administrators can open the screens; nothing they save is read.
| Setting | Where | Declared | Rendered | Persisted | Consumed | Effect |
|---|---|---|---|---|---|---|
DEFAULT_BRANCH | Default Selection | built inline into the applet container’s extension rows | Yes, branch drop-down | Broken — changing the value raises an error and nothing is saved | No consumer outside the settings screens | None |
DEFAULT_LOCATION | Default Selection | as above | Yes | Broken, as above | None | None |
Personal DEFAULT_BRANCH / DEFAULT_LOCATION | Personalization → Default Selection | as above | Yes | Broken — same commented subscription | None | None |
| Eight Field Settings toggles | Field Settings | No model | Yes | No — Save has no handler; the component class is empty | No | None (document-template residue) |
Model-only keys: none — the project has no settings model. Keys read at runtime without a declaration: none found.
Document behaviour settings
Not applicable — no documents.
Feature visibility / permissions
API permissions are the TNT_API_DM_MLM_* family (OWNER, ADMIN, MEMBER, CREATE, UPDATE, DELETE, READ for each of programs, participants, lineages, invitations, entity contacts and endpoint links) plus TNT_API_DM_MLM_REGISTRATION_{OWNER, ADMIN}. Accepting an invitation and following a QR link need no sign-in, by design: they are used by people who are not yet users.
No menu is restricted, and the applet has no client-side permissions of its own.
Fields
Program
| Field | Meaning | Required | Notes / validation |
|---|---|---|---|
| Program Name, Program Code | Display name and business key | Yes (UI, ≤255 each) | Backend does not check uniqueness of either — MLMErrors declares MLM_PROGRAM_HDR_OBJECT_CODE_ALREADY_EXISTS but the validator never raises it; DDL has no unique constraint. The commission report prints the program’s name, so two programs sharing a name cannot be told apart there — check the Programs listing before you reuse one |
| Start Date, End Date | The campaign window | Yes | Backend rejects null date_start / date_end; DDL date_end NOT NULL. Nothing else reads the window — an expired program still accepts participants and invitations |
| Commission Scheme | A scheme from Commission Scheme | No | Must exist if given; set at create only; not read by the commission run (see Lifecycle) |
| Website | A storefront | No | Checked to exist, then read by nothing: the QR link goes to URL Forward, and a storefront sign-up finds the program through the inviter’s key, whichever website the visitor registered on. The picker lists the first 100 websites only |
| URL Forward | The public landing URL the QR link redirects to | No | Must contain the literal <URL-KEY>; otherwise the redirect endpoint answers 404 |
| Status | ACTIVE / INACTIVE | Yes | Default ACTIVE |
Participant — the form also carries Program (read-only) and Object Type, which offers MEMBERSHIP only.
| Field | Meaning | Required | Notes / validation |
|---|---|---|---|
| Participant | The membership card, picked on the Partcipant tab (spelled that way on screen) | Yes | Unique per program + entity + member — MLM_PROGRAM_PARTICIPANT_HDR_OBJECT_VIOLATE_UNIQUE_CONSTRAINTS_ON_PROGRAM_HDR_MEMBER_GUID_ENTITY_HDR…; program, entity and member guids must exist |
| Date Effective | From when the participant counts | No — left empty, the server fills in the moment of saving | Delays nothing. Only the listing’s date filter reads it; the commission run counts a participant from the moment the row is ACTIVE, whatever date this says |
| Referral Source | The messaging channel that brought the participant | No | Stored as the channel’s id and shown as its name. A storefront sign-up writes the word INVITE instead, which matches no channel, so those participants show no referral source. The lineage job copies this value onto the downline’s lineage row |
| Status | ACTIVE / INACTIVE | Yes | Governs the buying side of commission. The commission run finds a buyer’s programs through ACTIVE participant rows only, so an INACTIVE participant’s purchases pay no upline. It does not stop that person being paid: the run climbs the tree through lineage rows and never reads an upline’s participant status — see Lineage Status |
| Invite URL key | Random key generated on create | — | Not shown as a field; unique in DDL; the storefront looks the inviter up by guid or this key |
Lineage — the form also carries Lineage Type, a label nothing that pays commission reads: the form sends Membership, and the lineage job copies the invitation’s own type (MEMBERSHIP, ENTITY or ENTITY_CONTACT).
| Field | Meaning | Required | Notes / validation |
|---|---|---|---|
| Upline | The parent participant | Yes | Parent and child participant, entity and member guids must all exist; parent ≠ child |
| Downline | The child participant | Yes | Unique per program + child — a downline has exactly one upline in a program |
| Referral Source | Channel that made the link | No | Left empty on the form, the link saves with an empty referral source — the server rejects only a missing value, which the form never sends. …REFERRAL_SOURCE_IS_NULL_OR_EMPTY comes from an API call that omits the field |
| Status | ACTIVE / INACTIVE | Yes | This is the switch the commission run obeys. It climbs from the buyer through ACTIVE lineage rows only, so setting a row INACTIVE stops the climb there: nobody above that link is paid on sales below it, and the downline’s own sales stop paying their upline. To stop paying one person while keeping the tree, a person has to decide which link to cut — the run offers no per-person switch |
| Level | Depth under the root | — | Computed, not entered: parent’s level + 1, or 1 when the parent has no lineage row; must stay within 0…10, also checked on delete |
Invitation
| Field | Meaning | Required | Notes / validation |
|---|---|---|---|
| Lineage Type | Where the invitee comes from: MEMBERSHIP, ENTITY, ENTITY_CONTACT | Yes | Reveals a tab — Invite from Members / Invite from Entity / Invite from Entity Contacts — a multi-select; one invitation row is created per selected invitee |
| Program, Inviter | Which program, and the participant doing the inviting | Yes | The inviter picker lists the chosen program’s participants. The inviter decides what the message carries: its link is the program’s URL Forward resolved with the inviter’s key, and an e-mail invitation is copied to the inviter’s entity e-mail |
| Expiry Date | When the invitation lapses | Yes (UI) | Not enforced — nothing sets EXPIRED, and the send processor picks every CREATED invitation regardless of the date |
| Invite Type | PARENT (sent by an inviter) or SELF_CLICKED (came in through a QR link) | No | — |
| Invite Status | CREATED, SENT, ACCEPTED, REJECTED, EXPIRED, LINEAGE_UPDATED (MLMConstants.InvitationStatus) | No | Pick Created when you create one. The form has no default and the server sets none, and the sending job picks only CREATED — an invitation saved with this blank is never sent and nothing says so. After that the processors drive it; the form lets an administrator set any value by hand |
| Status | ACTIVE / INACTIVE | Yes (no default on the form) | Both jobs pick ACTIVE invitations only, so INACTIVE is how you stop one from being sent or turned into a lineage row |
| Invitee (edit only) | The child member / entity / contact | — | Existence checks only if present; no e-mail or phone format check |
Entity Contact
| Field | Meaning | Required | Notes / validation |
|---|---|---|---|
| Name | The person | Yes (≤255) | Backend rejects empty |
| Parent Entity | The customer entity they belong to | No (UI) | Must exist if given. A grouping for your own listing: an invitation made from a contact copies the contact’s e-mail and phone at the moment you create it and carries no parent entity, and later edits to the contact do not reach invitations already made |
| Email, Phone, Status | How to reach them, and whether the contact is current | E-mail or phone: at least one; Status: yes | Format validated; duplicate e-mail / phone error codes exist in MLMErrors but are not checked. INACTIVE does not keep a contact out of an invitation — the invitee picker lists every contact, with the status shown beside it |
Communication Endpoint Link
| Field | Meaning | Required | Notes / validation |
|---|---|---|---|
| Name | Label | Yes | Not sent — the create screen cannot save at all (Troubleshooting). What the sending job reads from a link is its code, which must be the channel in lowercase — sms, email or whatsapp — and no screen sets it |
| Endpoint | A contact-centre endpoint (e-mail, SMS, WhatsApp) | Yes | Program and endpoint must exist; unique per program + endpoint. The sending job does not send from this endpoint: the sender and the message template come from the scheduled job’s own settings |
| Priority | Meant as send order, lowest first | Optional in the UI, required by the backend | Empty fails with …PRIORITY_IS_NULL_OR_EMPTY — but see Troubleshooting: the create screen cannot reach the backend at all. It orders nothing today: the send job fails as soon as it has two links to sort |
| Status | ACTIVE / INACTIVE | Yes | Keep exactly one link ACTIVE per program. The send job reads ACTIVE links only, and with two or more it fails before sending anything; there is no fallback from one link to the next |
Lifecycle and effects
Nothing posts, nothing moves. No record in this applet is a document: there is no journal, no stock movement, nothing for VOID to reverse. Every record is simply ACTIVE or INACTIVE.
How a downline joins — two paths
stateDiagram-v2
[*] --> CREATED: invitation saved in the applet
CREATED --> SENT: the invitation-sending job sends via the program's endpoint links
SENT --> ACCEPTED: invitee registers through the invitation link
[*] --> ACCEPTED: QR / storefront sign-up with the inviter's key (SELF_CLICKED)
ACCEPTED --> LINEAGE_UPDATED: the lineage job writes the lineage row
LINEAGE_UPDATED --> [*]
note right of CREATED
REJECTED and EXPIRED exist as statuses
but nothing sets them automatically
end note
Path 1 — an invitation sent by the applet. You create an invitation for one or more invitees, with Invite Status Created. The scheduled job MLM_INVITATION_LINK_PROCESSOR picks every ACTIVE invitation whose Invite Status is CREATED and reads the program’s ACTIVE endpoint link, whose code names the channel. The endpoint it sends from and the contact-centre template it uses are not taken from the link: both are set in the scheduled job’s own settings, one pair per channel. The template’s <URL-KEY> is replaced by the program’s URL Forward resolved for the inviter, the inviter’s entity e-mail is copied in, and the message goes by SMS (needs an invitee phone), e-mail (needs an invitee e-mail) or WhatsApp. On a 2xx reply the status becomes SENT. There is no fallback: a program with two or more ACTIVE links fails the run before anything is sent, and a send that fails — an e-mail link with an invitee who has only a phone, say — stops the run at that invitation, which stays CREATED and is tried again on the next run until someone fixes it or sets it INACTIVE. Whoever owns the programme checks the Invitations listing after each run: anything still at CREATED has not gone. The invitee opens the link and registers — first and last name, e-mail or phone, a password and a referral source — without needing to sign in. If no confirmed login exists for that e-mail or phone, a new login is created and a six-digit code is e-mailed or texted, valid for eight hours, which the invitee enters with their password to finish. With or without that step, the platform finds or creates the customer entity, finds or creates the membership card, creates the participant, and sets the matching invitation — same inviter and program, same e-mail or phone — to ACCEPTED with the child member and entity filled in. No match raises NO INVITATION CONTAINER COULD BE FOUND.
Path 2 — a QR or shared link. Each participant’s QR tab carries a personal link that needs no sign-in. It redirects to the program’s URL Forward with <URL-KEY> replaced by the participant’s key, or fails with not found when the URL Forward lacks the placeholder or is not a valid URL. When the visitor registers on the storefront with that key, the registration looks the inviter up, creates the new user’s entity, membership card and participant with referral source INVITE, and writes an invitation that is already ACCEPTED, type SELF_CLICKED, lineage type MEMBERSHIP. A website set to enforce MLM for every registration turns a missing key into a hard error that refuses the registration.
Both paths end the same way. The scheduled job INVITATION_INSERTION_INTO_LINEAGE_PROCESSOR takes every ACTIVE invitation at ACCEPTED that carries a child member and entity and a parent participant, member and entity, requires the child to already be a participant (it fails otherwise), creates the lineage row — computing its level — and sets the invitation to LINEAGE_UPDATED.
Lineage maintenance. Changing a row’s upline re-levels its whole subtree; deleting a row re-levels the descendants. Membership Admin’s MLM tab shows the chain each way.
Where the money is. The sales-commission posting run reads the lineage only for a scheme whose Commission Type is MLM: it loads the buyer entity’s participants, walks the lineage upward level by level (level 0 is the buyer, then each parent), and writes one intermediate commission line per upline per sales line with the program, level, member and upline entity, skipping levels whose treatment is zero. The scheme applied is the one the commission cycle selects; the program’s own Commission Scheme field is not consulted — the report uses the program only to print its name. The member’s referral code on the membership card is a separate mechanism that MLM does not use.
Every record in this applet can also be read and written through the API, and a signed-in user can enrol themselves in a program and list their own participations — see the developer documentation.
Check that the scheme you are building is lawful, because nothing here will
This applet will happily build a tree ten levels deep and pay every level of it, and it makes no distinction between a scheme that pays on sales and a scheme that pays on recruitment. That distinction is the one a regulator cares about, and in Malaysia a direct-selling or multi-level scheme is licensed rather than merely registered. The commission run reads the lineage and the scheme’s per-level treatments and asks nothing else.
So there is one question the software cannot answer and will never raise: what is the upline actually being paid for? If the treatment at each level is driven by a downline’s sales, the tree is a commission structure. If it is driven by the act of recruiting — a payment that exists whether or not the new participant ever buys anything — it is a different thing in law, and no configuration on this screen makes it safe.
Who and when: whoever owns the programme takes the scheme design to the company’s legal adviser before the first invitation is sent, not after the tree exists — because a lineage is easy to build and hard to unwind, and the participants in it are customers. The evidence they need is small: the Commission Scheme’s per-level treatments, what each treatment is calculated on, and the maximum depth. They know the review is finished when they can say, for each level, which sale the payment came from.
Related applets
- Membership Admin — participants are its membership cards; its member edit screen’s MLM and Referrals tabs show the tree this applet builds.
- Commission Scheme — the
MLMcommission type and its per-level treatments (up to ten uplines) are defined there. - Sales Commission — the posting run that walks the lineage and the report that shows MLM Program and MLM Member per line.
- Customer Maintenance and Entity — the entities behind participants and entity contacts; acceptance creates a customer entity when none exists.
- Unified Contact Center — the e-mail, SMS and WhatsApp endpoints and message templates the invitation processor sends through.
- CP Commerce Admin — the storefront whose sign-up completes the QR path and whose post-registration settings can make the inviter key mandatory.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Nothing happens when you save a Communication Endpoint link; invitations never leave CREATED | Creating a link from this screen does nothing, and the edit screen’s Save and Delete act on entity contacts, not links | Create the links through the API; do not use the edit screen’s Delete, which targets an entity contact |
| Payout opens a “page not found” screen | The menu entry points at a route (generic) that does not exist | None in the applet; commission payouts are in Sales Commission |
| Changing Default Branch or Location throws an error and nothing is saved | The settings screen never loads the applet container it writes into | DEFAULT_BRANCH / DEFAULT_LOCATION are read by nothing in this applet — they appear only in its two settings components, and the backend reads neither. Leave the screen alone |
Lineage save through the API fails with …REFERRAL_SOURCE_IS_NULL_OR_EMPTY | The call left out the referral source field; the server rejects a missing value (an empty one passes, which is why the form never meets this) | Send a referral source |
Invitations stay at CREATED and nobody receives anything | Invite Status was left blank (never picked), the program has no ACTIVE link or more than one, the link’s code is not sms / email / whatsapp, or one unsendable invitation is stopping each run | Set Invite Status to Created; keep one ACTIVE link; fix or set INACTIVE the invitation the run stops at |
Endpoint-link save fails with …PRIORITY_IS_NULL_OR_EMPTY (via API) | Priority is NOT NULL in the table and checked by the validator | Send a priority |
…VIOLATE_UNIQUE_CONSTRAINTS_ON_PROGRAM_HDR_MEMBER_GUID_ENTITY_HDR… on participant create | The member is already a participant of this program | Open the existing participant |
MLM_LINEAGE_HDR_OBJECT_PARENT_AND_CHILD_PARTICIPANT_GUID_IS_THE_SAME / …VIOLATE_UNIQUE_CONSTRAINTS | Upline equals downline, or the downline already has an upline in this program | Correct the pair; delete the old link first to move a downline |
MLM_LINEAGE_HDR_OBJECT_MAX_OR_MIN_LEVEL_IS_BREACHED | The new link would put someone deeper than level 10 | Attach the downline higher in the tree |
| An invitation past its Expiry Date is still sent, or still accepted | Expiry is stored but never enforced; EXPIRED and REJECTED are never set automatically | Set the invitation INACTIVE (or its status to EXPIRED by hand) to stop it |
| An accepted invitation never becomes a lineage row | The child is not yet a participant, or the invitation lacks child member / entity — the lineage processor skips it | Enrol the child as a participant; if the status was set to ACCEPTED by hand, fill in the invitee |
Acceptance fails with NO INVITATION CONTAINER COULD BE FOUND | The e-mail or phone entered on the acceptance page differs from the one on the invitation, or the inviter / program does not match | Accept with the invited address, or create a matching invitation |
| The QR link answers 404 or “Invalid URL” | The program’s URL Forward is empty, is not a valid URL, or lacks <URL-KEY> | Set URL Forward to the storefront landing page with <URL-KEY> in it |
| Storefront sign-up rejected for a missing inviter key | The website enforces an inviter key on every registration and the visitor arrived without one | Register through a participant’s link, or turn off enforcement on the website’s post-registration settings |
| E-mail invitations arrive formatted like the SMS template | The send processor passes the phone message-type code to the e-mail branch | Make the phone template usable by e-mail until the processor is corrected |
| Two programs with the same code | No code or name uniqueness check server-side | Keep codes unique yourself |
| Uplines are paid under a scheme other than the one on the program | The commission run uses the cycle’s scheme; the program’s Commission Scheme field is informational | Configure the intended MLM scheme in the commission cycle |
Related documentation
- Membership module
- Membership applets
- Commission Scheme — the
MLMcommission type - Integrate an external system — headers for the MLM API