nx_crm.nx source
↩ module page · 200 lines · 11083 B
1// nx_crm.nx -- PROFESSIONAL CRM PIPELINE (rung 2 of Nishi CRM/Relationships to SOTA). Operator 2026-07-08:
2// "clients like andelinwest.com can use a state of the art crm". The sales/deal layer on top of the nx_relate
3// entity graph: an ACCOUNT is a company entity you do business with; an OPPORTUNITY (deal) has an amount, a close
4// date, and a PIPELINE-STAGE; each stage carries a win PROBABILITY + a forecast category, so the DASHBOARD shows a
5// weighted forecast (Salesforce OpportunityStage model, verified 2026-07-08). Plus ACTIVITY (tasks/calls/meetings on
6// a record) + SEQUENCE (a follow-up cadence that auto-stops on reply). Composes with nx_relate (accounts + contacts
7// are entities) + nx_send (reach any contact) + nx_vizsla (timeline/reminders).
8// NOTE (research reframe): long-term these become object/edge DEFINITIONS on the flexible nx_relate engine, not a
9// hardcoded silo -- this focused organ ships the value now + is refactorable to engine-config (rework-not-retire).
10// Pure logic + a self-test gate (argless = selftest). Persistence = next rung.
11// Commands: stages | account <tenant> <name> | opp <tenant> <name> <account> <amount> <stage> | activity <tenant> <record> <type> <subject> | sequence <name> <steps> | dashboard | selftest
12// expect_exit: 0 license_tier: ORIGINAL
13import "nx_syscalls.nx"
14const K_MAGIC_50000: i64 = 50000
15const K_MAGIC_120000: i64 = 120000
16const K_MAGIC_30000: i64 = 30000
17const K_MAGIC_20000: i64 = 20000
18const K_MAGIC_10000: i64 = 10000
19const K_MAGIC_122500: i64 = 122500
20
21func slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
22func p(s: *u8) -> i64 { sys_write(1, s, slen(s)); return 0 }
23func pn(v: i64) -> i64 {
24 var m: i64 = v; if m < 0 { p("-" as *u8); m = 0 - m }
25 let t: *u8 = sys_mmap(24); var k: i64 = 0; if m == 0 { t[0] = 48 as u8; k = 1 }
26 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
27 let o: *u8 = sys_mmap(24); var i: i64 = 0; while i < k { o[i] = t[k-1-i]; i = i + 1 } sys_write(1, o, k); return 0
28}
29func seq(a: *u8, b: *u8) -> i64 {
30 var i: i64 = 0
31 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 }
32 if b[i] != (0 as u8) { return 0 }
33 return 1
34}
35
36// ---- PIPELINE-STAGE: name -> win probability (percent) + forecast category ----
37func stage_valid(s: *u8) -> i64 {
38 if seq(s, "Lead" as *u8) == 1 { return 1 }
39 if seq(s, "Qualified" as *u8) == 1 { return 1 }
40 if seq(s, "Proposal" as *u8) == 1 { return 1 }
41 if seq(s, "Negotiation" as *u8) == 1 { return 1 }
42 if seq(s, "ClosedWon" as *u8) == 1 { return 1 }
43 if seq(s, "ClosedLost" as *u8) == 1 { return 1 }
44 return 0
45}
46func stage_prob(s: *u8) -> i64 {
47 if seq(s, "Lead" as *u8) == 1 { return 10 }
48 if seq(s, "Qualified" as *u8) == 1 { return 25 }
49 if seq(s, "Proposal" as *u8) == 1 { return 50 }
50 if seq(s, "Negotiation" as *u8) == 1 { return 75 }
51 if seq(s, "ClosedWon" as *u8) == 1 { return 100 }
52 if seq(s, "ClosedLost" as *u8) == 1 { return 0 }
53 return 0 - 1
54}
55func stage_forecast(s: *u8) -> *u8 {
56 if seq(s, "Lead" as *u8) == 1 { return "Pipeline" as *u8 }
57 if seq(s, "Qualified" as *u8) == 1 { return "Pipeline" as *u8 }
58 if seq(s, "Proposal" as *u8) == 1 { return "BestCase" as *u8 }
59 if seq(s, "Negotiation" as *u8) == 1 { return "Commit" as *u8 }
60 if seq(s, "ClosedWon" as *u8) == 1 { return "Closed" as *u8 }
61 if seq(s, "ClosedLost" as *u8) == 1 { return "Omitted" as *u8 }
62 return "?" as *u8
63}
64func stage_open(s: *u8) -> i64 {
65 if seq(s, "ClosedWon" as *u8) == 1 { return 0 }
66 if seq(s, "ClosedLost" as *u8) == 1 { return 0 }
67 return 1
68}
69
70func cmd_stages() -> i64 {
71 p("=== NX-CRM PIPELINE-STAGE ladder (stage -> probability -> forecast) ===\n" as *u8)
72 let st: *i64 = sys_mmap(8 * 8) as *i64
73 st[0]="Lead" as *u8 as i64; st[1]="Qualified" as *u8 as i64; st[2]="Proposal" as *u8 as i64; st[3]="Negotiation" as *u8 as i64; st[4]="ClosedWon" as *u8 as i64; st[5]="ClosedLost" as *u8 as i64
74 var i: i64 = 0
75 while i < 6 {
76 let s: *u8 = st[i] as *u8
77 p(" " as *u8); p(s); p(" prob=" as *u8); pn(stage_prob(s)); p("% forecast=" as *u8); p(stage_forecast(s)); p("\n" as *u8)
78 i = i + 1
79 }
80 return 0
81}
82func cmd_account(tenant: *u8, name: *u8) -> i64 {
83 p("ACCOUNT tenant=" as *u8); p(tenant); p(" name=" as *u8); p(name); p(" (= a company entity in nx_relate)\n" as *u8); return 0
84}
85func cmd_opp(tenant: *u8, name: *u8, account: *u8, amount: i64, stage: *u8) -> i64 {
86 if stage_valid(stage) == 0 { p("OPPORTUNITY ERROR unknown stage=" as *u8); p(stage); p(" -- fail loud\n" as *u8); return 1 }
87 if amount < 0 { p("OPPORTUNITY ERROR negative amount -- fail loud\n" as *u8); return 1 }
88 let w: i64 = amount * stage_prob(stage) / 100
89 p("OPPORTUNITY tenant=" as *u8); p(tenant); p(" deal=" as *u8); p(name); p(" account=" as *u8); p(account)
90 p(" amount=" as *u8); pn(amount); p(" PIPELINE-STAGE=" as *u8); p(stage); p(" prob=" as *u8); pn(stage_prob(stage))
91 p("% weighted=" as *u8); pn(w); p(" forecast=" as *u8); p(stage_forecast(stage)); p("\n" as *u8)
92 return 0
93}
94func cmd_activity(tenant: *u8, record: *u8, kind: *u8, subject: *u8) -> i64 {
95 var ok: i64 = 0
96 if seq(kind, "call" as *u8) == 1 { ok = 1 }
97 if seq(kind, "email" as *u8) == 1 { ok = 1 }
98 if seq(kind, "meeting" as *u8) == 1 { ok = 1 }
99 if seq(kind, "task" as *u8) == 1 { ok = 1 }
100 if ok == 0 { p("ACTIVITY ERROR unknown type=" as *u8); p(kind); p(" (call|email|meeting|task) -- fail loud\n" as *u8); return 1 }
101 p("ACTIVITY tenant=" as *u8); p(tenant); p(" on=" as *u8); p(record); p(" type=" as *u8); p(kind); p(" subject=" as *u8); p(subject); p("\n" as *u8)
102 return 0
103}
104func cmd_sequence(name: *u8, steps: i64) -> i64 {
105 if steps < 1 { p("SEQUENCE ERROR needs >=1 step -- fail loud\n" as *u8); return 1 }
106 p("SEQUENCE name=" as *u8); p(name); p(" steps=" as *u8); pn(steps); p(" auto-stop-on-reply=yes (channels: email/call/postcard via nx_send)\n" as *u8)
107 return 0
108}
109
110// DASHBOARD: weighted forecast over a demo pipeline (persistence is the next rung).
111// returns the weighted OPEN forecast (the gate checks it).
112func cmd_dashboard() -> i64 {
113 p("=== NX-CRM DASHBOARD -- Andelin West pipeline (weighted forecast) ===\n" as *u8)
114 let nm: *i64 = sys_mmap(8 * 8) as *i64
115 let am: *i64 = sys_mmap(8 * 8) as *i64
116 let sg: *i64 = sys_mmap(8 * 8) as *i64
117 nm[0]="Westfield Renovation" as *u8 as i64; am[0]=K_MAGIC_50000; sg[0]="Proposal" as *u8 as i64
118 nm[1]="Riverside Contract" as *u8 as i64; am[1]=K_MAGIC_120000; sg[1]="Negotiation" as *u8 as i64
119 nm[2]="Downtown Lease" as *u8 as i64; am[2]=K_MAGIC_30000; sg[2]="Qualified" as *u8 as i64
120 nm[3]="Old Bid" as *u8 as i64; am[3]=K_MAGIC_20000; sg[3]="ClosedLost" as *u8 as i64
121 var open_total: i64 = 0
122 var weighted: i64 = 0
123 var won: i64 = 0
124 var i: i64 = 0
125 while i < 4 {
126 let s: *u8 = sg[i] as *u8
127 let a: i64 = am[i]
128 p(" " as *u8); p(nm[i] as *u8); p(" $" as *u8); pn(a); p(" " as *u8); p(s); p(" (" as *u8); pn(stage_prob(s)); p("%)\n" as *u8)
129 if stage_open(s) == 1 { open_total = open_total + a; weighted = weighted + (a * stage_prob(s) / 100) }
130 if seq(s, "ClosedWon" as *u8) == 1 { won = won + a }
131 i = i + 1
132 }
133 p(" ----\n open pipeline = $" as *u8); pn(open_total); p(" weighted forecast = $" as *u8); pn(weighted); p(" won = $" as *u8); pn(won); p("\n" as *u8)
134 return weighted
135}
136
137func cmd_selftest() -> i64 {
138 p("=== NX-CRM SELFTEST (ACCOUNT + OPPORTUNITY + PIPELINE-STAGE + ACTIVITY + SEQUENCE + DASHBOARD) ===\n" as *u8)
139 var ok: i64 = 1
140 // stage validity + probability (pos + neg)
141 if stage_valid("Proposal" as *u8) != 1 { ok = 0 }
142 if stage_valid("ClosedWon" as *u8) != 1 { ok = 0 }
143 if stage_valid("Nonsense" as *u8) != 0 { ok = 0 }
144 if stage_prob("Negotiation" as *u8) != 75 { ok = 0 }
145 if stage_prob("ClosedLost" as *u8) != 0 { ok = 0 }
146 if stage_prob("Lead" as *u8) != 10 { ok = 0 }
147 if stage_open("Proposal" as *u8) != 1 { ok = 0 }
148 if stage_open("ClosedWon" as *u8) != 0 { ok = 0 }
149 // commands
150 let a1: i64 = cmd_account("andelinwest" as *u8, "Westfield Group" as *u8)
151 let o1: i64 = cmd_opp("andelinwest" as *u8, "Westfield Renovation" as *u8, "Westfield Group" as *u8, K_MAGIC_50000, "Proposal" as *u8)
152 let obad: i64 = cmd_opp("andelinwest" as *u8, "Bad Deal" as *u8, "X" as *u8, K_MAGIC_10000, "Nonsense" as *u8)
153 let ac1: i64 = cmd_activity("andelinwest" as *u8, "Westfield Renovation" as *u8, "call" as *u8, "Discuss scope" as *u8)
154 let acbad: i64 = cmd_activity("andelinwest" as *u8, "X" as *u8, "smoke-signal" as *u8, "nope" as *u8)
155 let sq1: i64 = cmd_sequence("New lead nurture" as *u8, 4)
156 if a1 != 0 { ok = 0 }
157 if o1 != 0 { ok = 0 }
158 if obad != 1 { ok = 0 }
159 if ac1 != 0 { ok = 0 }
160 if acbad != 1 { ok = 0 }
161 if sq1 != 0 { ok = 0 }
162 // dashboard weighted forecast must equal the known demo total
163 let wf: i64 = cmd_dashboard()
164 if wf != K_MAGIC_122500 { ok = 0 }
165 p("NX-CRM-SELFTEST stages=6 weighted-forecast=" as *u8); pn(wf); p(" " as *u8)
166 if ok == 1 { p("verdict=GREEN\n" as *u8); return 0 }
167 p("verdict=RED\n" as *u8)
168 return 1
169}
170
171// PURE-CORE entry (no main): consumers = _hdl_build/nx_crm_gate (CLI+selftest wrapper) + nx_crm_store (persistence).
172func cm_cli(argc: i64, argv: *i64) -> i64 {
173 if argc < 2 { return cmd_selftest() }
174 let cmd: *u8 = argv[1] as *u8
175 if seq(cmd, "selftest" as *u8) == 1 { return cmd_selftest() }
176 if seq(cmd, "stages" as *u8) == 1 { return cmd_stages() }
177 if seq(cmd, "dashboard" as *u8) == 1 { let w: i64 = cmd_dashboard(); return 0 }
178 if seq(cmd, "account" as *u8) == 1 {
179 if argc < 4 { p("usage: account <tenant> <name>\n" as *u8); return 1 }
180 return cmd_account(argv[2] as *u8, argv[3] as *u8)
181 }
182 if seq(cmd, "opp" as *u8) == 1 {
183 if argc < 7 { p("usage: opp <tenant> <name> <account> <amount> <stage>\n" as *u8); return 1 }
184 var amt: i64 = 0; var i: i64 = 0; let a: *u8 = argv[5] as *u8
185 while a[i] != (0 as u8) { let d: i64 = (a[i] as i64) - 48; if d >= 0 { if d <= 9 { amt = amt * 10 + d } } i = i + 1 }
186 return cmd_opp(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, amt, argv[6] as *u8)
187 }
188 if seq(cmd, "activity" as *u8) == 1 {
189 if argc < 6 { p("usage: activity <tenant> <record> <type> <subject>\n" as *u8); return 1 }
190 return cmd_activity(argv[2] as *u8, argv[3] as *u8, argv[4] as *u8, argv[5] as *u8)
191 }
192 if seq(cmd, "sequence" as *u8) == 1 {
193 if argc < 4 { p("usage: sequence <name> <steps>\n" as *u8); return 1 }
194 var stp: i64 = 0; var i: i64 = 0; let a: *u8 = argv[3] as *u8
195 while a[i] != (0 as u8) { let d: i64 = (a[i] as i64) - 48; if d >= 0 { if d <= 9 { stp = stp * 10 + d } } i = i + 1 }
196 return cmd_sequence(argv[2] as *u8, stp)
197 }
198 p("NX-CRM usage: stages | account | opp | activity | sequence | dashboard | selftest\n" as *u8)
199 return 1
200}