forked from TurboAsterix/equateplus-moneymoney
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathEquatePlus.lua
More file actions
731 lines (663 loc) · 29.4 KB
/
Copy pathEquatePlus.lua
File metadata and controls
731 lines (663 loc) · 29.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
-- Requires MoneyMoney 2.4.72 or later (native QR display with poll support)
-- Use participant login entry to avoid outage landing page
local url="https://www.equateplus.com/EquatePlusParticipant2/?login"
function rnd()
return math.random(10000000,99999999)
end
local function urlencode(s)
if s == nil then return "" end
s = tostring(s)
s = string.gsub(s, "([^A-Za-z0-9%-_%.~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
return s
end
local dcHost = "https://www.equateplus.com"
local reportOnce
local Version=4.00
local CSRF_TOKEN=nil
local CSRF2_TOKEN=nil
local connection
local debugging=true
local nosecrets=true
local cummulate=false
local html
local cId="eqp."..rnd()
local session_id
-- State for SMS-OTP authentication flow
local awaitingOtp=false
local otpPageHtml=nil
function startsWith(String,Start)
return string.sub(String,1,string.len(Start))==Start
end
function split(inputstr, sep)
if sep == nil then
sep = "%s"
end
local t={}
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
table.insert(t, str)
end
return t
end
function connectWithCSRF(method, url, postContent, postContentType, headers)
-- Normalize URL to selected datacenter host
local function normalize(u)
local host = dcHost or "https://www.equateplus.com"
u = u or ""
if string.match(u, "^https?://") then
-- absolute URL: replace host only
local path = string.match(u, "^https?://[^/]+(.*)$") or "/"
return host .. path
elseif string.sub(u, 1, 1) == "?" then
-- query-relative (e.g. "?login" from form action="?login"):
-- resolve against /EquatePlusParticipant2/ so we get the correct full path
return host .. "/EquatePlusParticipant2/" .. u
else
if string.sub(u, 1, 1) ~= "/" then u = "/" .. u end
return host .. u
end
end
local content
local respHeaders
-- Support Request object from HTML:submit()
if type(method) ~= 'string' then
local req = method
local u = normalize(req and req.url or url or "/")
local m = (req and req.method) or 'GET'
local body = (req and (req.postContent or req.body)) or postContent or ""
local ct = (req and (req.postContentType or req.mimeType)) or postContentType or "application/x-www-form-urlencoded"
local h = {}
-- Start from request headers if present
if req and req.headers then
for k, v in pairs(req.headers) do h[k] = v end
end
-- Merge explicit headers
if headers then
for k, v in pairs(headers) do h[k] = v end
end
h["Accept"] = h["Accept"] or "*/*"
-- For login orchestration endpoints, request JSON and mark XHR
if string.find(u, "%?login") then
h["Accept"] = "application/json, text/plain, */*"
h["X-Requested-With"] = h["X-Requested-With"] or "XMLHttpRequest"
if h["Referer"] == nil then
h["Referer"] = (dcHost or "https://www.equateplus.com") .. "/eqlogin/"
end
end
if string.find(u, "/EquatePlusParticipant2/services/") and h["Referer"] == nil then
h["Referer"] = (dcHost or "https://www.equateplus.com") .. "/EquatePlusParticipant2/"
end
if CSRF_TOKEN ~= nil then h['csrfpId']=CSRF_TOKEN else if debugging then print("without CSRF_TOKEN") end end
if CSRF2_TOKEN ~= nil then h["EQUATE-CSRF2-TOKEN-PARTICIPANT2"]=CSRF2_TOKEN end
content, charset, mimeType, filename, respHeaders = connection:request(m, u, body, ct, h)
else
-- Classic call signature
url = normalize(url)
postContentType=postContentType or "application/json"
if headers == nil then headers={} end
headers["Accept"] = headers["Accept"] or "*/*"
-- For login orchestration endpoints, request JSON and mark XHR
if string.find(url, "%?login") then
headers["Accept"] = "application/json, text/plain, */*"
headers["X-Requested-With"] = headers["X-Requested-With"] or "XMLHttpRequest"
if headers["Referer"] == nil then
headers["Referer"] = (dcHost or "https://www.equateplus.com") .. "/eqlogin/"
end
end
if string.find(url, "/EquatePlusParticipant2/services/") and headers["Referer"] == nil then
headers["Referer"] = (dcHost or "https://www.equateplus.com") .. "/EquatePlusParticipant2/"
end
if CSRF_TOKEN ~= nil then headers['csrfpId']=CSRF_TOKEN else if debugging then print("without CSRF_TOKEN") end end
if CSRF2_TOKEN ~= nil then headers["EQUATE-CSRF2-TOKEN-PARTICIPANT2"]=CSRF2_TOKEN end
if method == 'POST' then
if postContent == nil then postContent="" end
end
content, charset, mimeType, filename, respHeaders = connection:request(method, url, postContent, postContentType, headers)
end
-- Try to extract CSRF token from JSON and HTML patterns
local csrfpIdTemp = string.match(content, '"csrfpId"%s*:%s*"([^"]+)"')
if csrfpIdTemp == nil or csrfpIdTemp == '' then
csrfpIdTemp = string.match(content, 'csrfRegisterAjax%(%s*"csrfpId"%s*,%s*"([^"]+)"')
end
if csrfpIdTemp == nil or csrfpIdTemp == '' then
csrfpIdTemp = string.match(content, 'csrfModifyLinks%(%s*"csrfpId"%s*,%s*"([^"]+)"')
end
if csrfpIdTemp ~= nil and csrfpIdTemp ~= '' then
CSRF_TOKEN=csrfpIdTemp
end
-- Try multiple patterns to extract CSRF2
local csrf2Temp
csrf2Temp = string.match(content, "['\"]equateCsrfToken2['\"]%s*:%s*['\"]([^'\"]+)['\"]")
if csrf2Temp == nil or csrf2Temp == '' then
csrf2Temp = string.match(content, "name=['\"]EQUATE%-CSRF2%-TOKEN%-PARTICIPANT2['\"]%s+value=['\"]([^'\"]+)['\"]")
end
if csrf2Temp ~= nil and csrf2Temp ~= '' then
CSRF2_TOKEN = csrf2Temp
end
if debugging then
local headersToLog = {}
for k, v in pairs(respHeaders or {}) do
local kl = string.lower(tostring(k))
if nosecrets and (
kl == "set-cookie" or kl == "cookie" or kl == "authorization" or
kl == "equate-csrf2-token-participant2" or kl == "csrfpid" or
kl == "x-csrf-token" or kl == "x-auth-token"
) then
headersToLog[k] = "<redacted>"
else
headersToLog[k] = v
end
end
tprint(headersToLog)
-- lprint(content)
end
return content
end
WebBanking{
version=Version,
url=url,
services={"EquatePlus"},
description = "EquatePlus portfolio"
}
function SupportsBank (protocol, bankCode)
return protocol == ProtocolWebBanking and (
bankCode == "EquatePlus" or
bankCode == "EquatePlus SE" or
bankCode == "EquatePlus (cumulative)" or
bankCode == "EquatePlus SE (cumulative)"
)
end
function lprint(text)
repeat
print(" ",string.sub(text,1,60))
text=string.sub(text,61)
until text == ''
end
function tprint (tbl, indent)
if debugging then
if not indent then indent = 3 end
for k, v in pairs(tbl) do
local formatting = string.rep(" ", indent) .. k .. ": "
if type(v) == 'table' and indent < 9 then
print(formatting .. "table")
tprint(v,indent+3)
elseif type(v) == 'string' then
if nosecrets then
print(formatting .. "string'<redacted>'")
else
print(formatting .. "string'"..v.."'")
end
else
print(formatting .. type(v))
end
end
end
end
function InitializeSession2 (protocol, bankCode, step, credentials, interactive)
if step==1 then
-- Login.
debugging=false
cummulate=true
CSRF_TOKEN=nil
CSRF2_TOKEN=nil
connection = Connection()
local username=credentials[1]
local password=credentials[2]
if string.sub(username,1,1) == '#' then
print("Debugging, remove # char from username!")
username=string.sub(username,2)
debugging=true
end
if string.sub(username,1,1) == '#' then
print("Debugging, remove # chars from username!")
username=string.sub(username,2)
nosecrets=true
end
-- Helper to detect presence of login form or username field
local function hasLoginForm(doc)
return (doc:xpath("//*[@id='loginForm']"):length() > 0) or (doc:xpath("//input[@name='isiwebuserid']"):length() > 0)
end
-- get login page (avoid outage page). Try primary + datacenter fallbacks.
local function tryLoadLogin(u)
return HTML(connectWithCSRF("GET", u))
end
dcHost = "https://www.equateplus.com"
html = tryLoadLogin(url)
if not hasLoginForm(html) then
-- Outage screen or changed landing; attempt geo DCs
local tried = {
"https://www.emea.equateplus.com/EquatePlusParticipant2/?login",
"https://www.na.equateplus.com/EquatePlusParticipant2/?login",
"https://participant.tst.equateplus.com/EquatePlusParticipant2/?login" -- BT1 fallback (rare)
}
for _, u in ipairs(tried) do
-- Pin host to the candidate datacenter
dcHost = string.match(u, "^(https?://[^/]+)") or dcHost
local candidate = tryLoadLogin(u)
if hasLoginForm(candidate) then
html = candidate
break
end
end
end
if not hasLoginForm(html) then
return "EquatePlus plugin error: No login mask found!"
end
-- first login stage
-- print("login first stage")
html:xpath("//*[@id='eqUserId']"):attr("value", username)
html:xpath("//*[@id='submitField']"):attr("value","Continue Login")
html= HTML(connectWithCSRF(html:xpath("//*[@id='loginForm']"):submit()))
if not hasLoginForm(html) then return "EquatePlus plugin error: No login mask found!" end
-- second login stage: manual POST to include CSRF token in body
-- (HTML:submit() misses JS-injected hidden fields like csrfpId)
local function urlEncode(s)
return (s:gsub("([^%w%-%.%_%~ ])", function(c)
return string.format("%%%02X", string.byte(c))
end):gsub(" ", "+"))
end
local postBody = "isiwebuserid=" .. urlEncode(username) ..
"&isiwebpasswd=" .. urlEncode(password) ..
"&result=Continue"
if CSRF_TOKEN then
postBody = postBody .. "&csrfpId=" .. urlEncode(CSRF_TOKEN)
end
local content = connectWithCSRF(
"POST",
dcHost .. "/EquatePlusParticipant2/?login",
postBody,
"application/x-www-form-urlencoded"
)
html = HTML(content)
-- Detect SMS OTP flow
if string.find(content, 'id="otpCodeId"') or string.find(content, 'class="otpCodeSms"') or string.find(content, 'Security Step Code') then
awaitingOtp = true
otpPageHtml = html
-- Prompt for OTP via interactive callback if available
if interactive ~= nil then
local otp = nil
-- Simple string prompt
local ok1, val1 = pcall(function() return interactive("Please enter the SMS code.") end)
if ok1 and val1 and val1 ~= '' then otp = val1 end
-- Alternative prompt (some MoneyMoney versions)
if (not otp or otp == '') then
local ok2, val2 = pcall(function() return interactive({ title = "Security Code", challenge = "Please enter the SMS code." }) end)
if ok2 and val2 and val2 ~= '' then otp = val2 end
end
-- Submit OTP and continue
if otp and otp ~= '' then
otpPageHtml:xpath("//*[@id='otpCodeId']"):attr("value", otp)
otpPageHtml:xpath("//*[@id='submitField']"):attr("value","verify")
local afterContent = connectWithCSRF(otpPageHtml:xpath("//*[@id='loginForm']"):submit())
local after = HTML(afterContent)
local errTxt = after:xpath("//*[@id='ErrorMsg']"):text()
local otpErrTxt = after:xpath("//*[@id='OtpErrorMsg']"):text()
if (errTxt and errTxt ~= "") or (otpErrTxt and otpErrTxt ~= "") or string.find(afterContent, 'id="otpCodeId"') then
local msg = otpErrTxt or errTxt or "Verification failed."
return "Operation failed: " .. msg
end
awaitingOtp=false
otpPageHtml=nil
-- Finalize login like in the QR flow
connectWithCSRF("POST","https://www.equateplus.com/EquatePlusParticipant2/?login&_cId="..cId.."&_rId="..rnd(), "result=Continue", "application/x-www-form-urlencoded")
-- Seed CSRF2 by loading the participant home
connectWithCSRF("GET","https://www.equateplus.com/EquatePlusParticipant2/")
return nil
end
end
-- Fallback: open 2FA dialog; step 2 will read input
return {
title = "Security Code",
challenge = "Please enter the SMS code.",
label = "Code",
password = true,
default = ""
}
end
-- Fallback to FIDO/QR flow (ensure client/session ids for JSON orchestration)
local resp = connectWithCSRF(
"POST",
"https://www.equateplus.com/EquatePlusParticipant2/?login&_cId="..cId.."&_rId="..rnd(),
"isiwebuserid="..urlencode(username).."&isiwebpasswd=null&result=null",
"application/x-www-form-urlencoded"
)
local ok, json = pcall(function() return JSON(resp):dictionary() end)
if not ok or not json or not json["dispatchTargets"] or not json["dispatchTargets"][1] then
return "Operation failed: Unexpected authentication method (no dispatchTargets)."
end
local target = json["dispatchTargets"][1]
-- get qr code
json = JSON(connectWithCSRF("GET","https://www.equateplus.com/EquatePlusParticipant2/?login&o.dispatchTargetId.v="..target["id"].."&_cId="..cId.."&_rId="..rnd())):dictionary()
session_id = json["sessionId"]
local challenge = json["dispatcherInformation"]["response"]
-- request authentication
return {
title=target["name"],
challenge=challenge,
poll=true,
tanMethod={name="QR-Code"},
}
else
-- Handle second step for SMS-OTP if required
if awaitingOtp and otpPageHtml ~= nil then
-- Read OTP from MoneyMoney's challenge response (usually credentials[1])
local otp = nil
if credentials then
-- Common positions/keys
otp = credentials[1] or credentials["otp"] or credentials["tan"] or credentials[3]
end
-- As fallback (older MoneyMoney), ask via interactive dialog
if (not otp or otp == "") and interactive ~= nil then
local ok, value = pcall(function() return interactive("Please enter the SMS code.") end)
if ok then otp = value end
end
-- If still no OTP, request input (do not clear awaitingOtp)
if not otp or otp == "" then
return {
title = "Security Code",
challenge = "Please enter the SMS code.",
label = "Code",
password = true,
default = ""
}
end
otpPageHtml:xpath("//*[@id='otpCodeId']"):attr("value", otp)
otpPageHtml:xpath("//*[@id='submitField']"):attr("value","verify")
local content = connectWithCSRF(otpPageHtml:xpath("//*[@id='loginForm']"):submit())
local after = HTML(content)
local errTxt = after:xpath("//*[@id='ErrorMsg']"):text()
local otpErrTxt = after:xpath("//*[@id='OtpErrorMsg']"):text()
if (errTxt and errTxt ~= "") or (otpErrTxt and otpErrTxt ~= "") or string.find(content, 'id="otpCodeId"') then
local msg = otpErrTxt or errTxt or "Verification failed."
return "Operation failed: " .. msg
end
awaitingOtp=false
otpPageHtml=nil
-- Finalize login like in the QR flow
connectWithCSRF("POST","https://www.equateplus.com/EquatePlusParticipant2/?login&_cId="..cId.."&_rId="..rnd(), "result=Continue", "application/x-www-form-urlencoded")
-- Seed CSRF2 by loading the participant home
connectWithCSRF("GET","https://www.equateplus.com/EquatePlusParticipant2/")
return nil
end
-- Wait up to 30 seconds for verification (FIDO/QR)
local count = 0
while count < 30 do
local json = JSON(connectWithCSRF("GET","https://www.equateplus.com/EquatePlusParticipant2/?login&o.fidoUafSessionId.v="..session_id.."&_cId="..cId.."&_rId="..rnd())):dictionary()
print(json["status"])
if json["status"] == "succeeded" then
-- Complete login after verification
connectWithCSRF("POST","https://www.equateplus.com/EquatePlusParticipant2/?login&_cId="..cId.."&_rId="..rnd(), "result=Continue", "application/x-www-form-urlencoded")
-- Seed CSRF2 by loading the participant home
connectWithCSRF("GET","https://www.equateplus.com/EquatePlusParticipant2/")
return nil
end
if json["status"] == "failed_retry_please" then
return "Operation failed: Please retry."
end
if json["status"] == "failed" then
return "Operation failed"
end
MM.sleep(1)
count = count + 1
end
end
return "Operation failed: Authentication was not confirmed"
end
function ListAccounts (knownAccounts)
local user=JSON(connectWithCSRF("GET","https://www.equateplus.com/EquatePlusParticipant2/services/user/get?_cId="..cId.."&_rId="..rnd())):dictionary()
if debugging then tprint (user) end
-- Return array of accounts.
reportOnce=true
local account
local status,err = pcall( function()
account = {
name = "Equateplus "..user["companyId"],
--owner = user["participant"]["firstName"]["displayValue"].." "..user["participant"]["lastName"]["displayValue"],
accountNumber = user["participant"]["userId"],
bankCode = "equatePlus",
currency = user["reportingCurrency"]["code"],
portfolio = true,
type = AccountTypePortfolio
}
end)--pcall
bugReport(status,err,user)
return {account}
end
local function isLoginRedirect(content)
return content ~= nil and (
string.find(content, "eqp-login-application") ~= nil or
string.find(content, 'id="loginForm"') ~= nil or
string.find(content, 'id="eqUserId"') ~= nil
)
end
function RefreshAccount (account, since)
-- Try POST (preferred on some backends)
local summaryContent = connectWithCSRF(
"POST",
"https://www.equateplus.com/EquatePlusParticipant2/services/planSummary/get?_cId="..cId.."&_rId="..rnd(),
"{\"$type\":\"Object\"}",
"application/json;charset=UTF-8"
)
-- Detect session expiry / login redirect (April 2026 EquatePlus change)
if isLoginRedirect(summaryContent) then
print("EquatePlus: session expired or auth failed — got login page instead of portfolio data.")
print("Please trigger a new sync to re-authenticate.")
return {securities={}, balance=0}
end
local summary = JSON(summaryContent):dictionary()
-- Fallback to GET if no entries
if not summary or not summary["entries"] or #summary["entries"] == 0 then
local getContent = connectWithCSRF("GET","https://www.equateplus.com/EquatePlusParticipant2/services/planSummary/get?_cId="..cId.."&_rId="..rnd())
if isLoginRedirect(getContent) then
print("EquatePlus: GET also returned login page — session invalid.")
return {securities={}, balance=0}
end
summary = JSON(getContent):dictionary()
end
if not summary then
print("EquatePlus: planSummary response could not be parsed as JSON.")
return {securities={}, balance=0}
end
if not summary["entries"] then
print("EquatePlus: planSummary has no 'entries' field — API may have changed.")
print("Response keys:")
for k, _ in pairs(summary) do print(" key: " .. tostring(k)) end
return {securities={}, balance=0}
end
if debugging then tprint (summary) end
local securities = {}
reportOnce=true
local status,err = pcall( function()
for k,v in pairs(summary["entries"]) do
local details=JSON(connectWithCSRF("POST","https://www.equateplus.com/EquatePlusParticipant2/services/planDetails/get?_cId="..cId.."&_rId="..rnd(),"{\"$type\":\"EntityIdentifier\",\"id\":\""..v["id"].."\"}","application/json;charset=UTF-8")):dictionary()
if debugging then tprint (details) end
local planNameFallback = (details and details["name"]) or v["name"] or "EquatePlus Position"
local status,err = pcall( function()
for k,v in pairs(details["entries"]) do
local status,err = pcall( function()
for k,v in pairs(v["entries"]) do
local status,err = pcall( function()
local marketName=v["marketName"]
local marketPrice=v["marketPrice"]["amount"]
local pendingShare = (v["canTrade"] == false)
for k,v in pairs(v["entries"]) do
local status,err = pcall( function()
-- Support multiple quantity keys
local quantityKeyList = nil
quantityKeyList = {next = quantityKeyList, value = "QUANTITY"}
quantityKeyList = {next = quantityKeyList, value = "AVAIL_QTY"}
quantityKeyList = {next = quantityKeyList, value = "UNITS"}
quantityKeyList = {next = quantityKeyList, value = "AVAILABLE_UNITS"}
quantityKeyList = {next = quantityKeyList, value = "NET_UNITS"}
quantityKeyList = {next = quantityKeyList, value = "TOTAL_UNITS"}
quantityKeyList = {next = quantityKeyList, value = "LOCKED_QTY"}
quantityKeyList = {next = quantityKeyList, value = "LOCKED_PERF_QTY"}
local quantity = 0
local quantityKey = quantityKeyList
while quantityKey do
if v[quantityKey.value] and v[quantityKey.value]["amount"] then
quantity = v[quantityKey.value]["amount"]
break
end
quantityKey = quantityKey.next
end
-- Support multiple price keys
local purchasePrice = nil
local currencyOfPrice = nil
local priceKeyList = nil
priceKeyList = {next = priceKeyList, value = "SELL_PURCHASE_PRICE"}
priceKeyList = {next = priceKeyList, value = "COST_BASIS"}
priceKeyList = {next = priceKeyList, value = "MARKET_PRICE"}
priceKeyList = {next = priceKeyList, value = "PURCHASE_PRICE"}
local priceKey = priceKeyList
while priceKey do
if v[priceKey.value] and v[priceKey.value]["amount"] then
purchasePrice = v[priceKey.value]["amount"]
currencyOfPrice = v[priceKey.value]["unit"] and v[priceKey.value]["unit"]["code"] or nil
break
end
priceKey = priceKey.next
end
if purchasePrice ~= nil or quantity > 0 then
-- Support multiple date keys
local tradeTimestamp = nil
local dateKeyList = nil
dateKeyList = {next = dateKeyList, value = "ALLOC_DATE"}
dateKeyList = {next = dateKeyList, value = "TRANSACTION_DATE"}
local dateKey = dateKeyList
while dateKey do
if v[dateKey.value] and v[dateKey.value]["date"] then
-- Example: "2016-02-12T00:00:00.000"
local year, month, day = v[dateKey.value]["date"]:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)")
-- print(year .. "-" .. month .. "-" .. day)
tradeTimestamp=os.time({year=year,month=month,day=day})
break
end
dateKey = dateKey.next
end
-- Support multiple name keys
local name = nil
local nameKeyList = nil
nameKeyList = {next = nameKeyList, value = "VEHICLE"}
nameKeyList = {next = nameKeyList, value = "VEHICLE_DESCRIPTION"}
nameKeyList = {next = nameKeyList, value = "SECURITY"}
nameKeyList = {next = nameKeyList, value = "VEHICLE_NAME"}
local nameKey = nameKeyList
while nameKey and name == nil do
name = v[nameKey.value]
nameKey = nameKey.next
end
local secName = name or planNameFallback or "EquatePlus Position"
-- Future feature for MoneyMoney (confirmed 2022-02-10 by MRH):
-- requires a property similar to "booked" for accounts
if pendingShare then
print("These shares are not tradable: " .. tostring(secName))
end
local security = {
-- String name: Security name
name=secName,
-- String isin: ISIN
-- String securityNumber: WKN
-- String market: Exchange
market=marketName,
-- String currency: Currency for nominal or nil for units
-- Number quantity: Nominal amount or units
quantity=quantity,
-- Number amount: Position value in account currency
-- Number originalCurrencyAmount: Position value in original currency
-- Number exchangeRate: FX rate
-- Number tradeTimestamp: Quote timestamp (POSIX)
tradeTimestamp=tradeTimestamp,
-- Number price: Current price
price=marketPrice,
-- String currencyOfPrice: Price currency (if different)
currencyOfPrice=currencyOfPrice,
-- Number purchasePrice: Purchase price
purchasePrice=purchasePrice,
-- String currencyOfPurchasePrice: Purchase price currency (if different)
}
if cummulate then
if securities[secName] == nil then
if security['purchasePrice'] ~= nil then
security['sumPrice']=security['purchasePrice']*quantity
end
securities[secName]=security
table.insert(securities,security)
else
securities[secName]['quantity']=securities[secName]['quantity']+quantity
if security['purchasePrice'] ~= nil and securities[secName]['sumPrice'] ~= nil then
securities[secName]['sumPrice']=securities[secName]['sumPrice']+security['purchasePrice']*quantity
securities[secName]['purchasePrice']=securities[secName]['sumPrice']/securities[secName]['quantity']
else
securities[secName]['sumPrice']=nil
securities[secName]['purchasePrice']=nil
end
end
else
table.insert(securities,security)
end
end
end) --pcall
bugReport(status,err,v)
end
end)--pcall
bugReport(status,err,v)
end
end) --pcall
bugReport(status,err,v)
end
end) --pcall
bugReport(status,err,v)
end
end) --pcall
bugReport(status,err,details)
return {securities=securities}
end
function FetchStatements (accounts, knownIdentifiers)
local statements = {}
-- Load postbox page.
local libraryContent = connectWithCSRF("POST","https://www.equateplus.com/EquatePlusParticipant2/services/documents/library?_cId="..cId.."&_rId="..rnd(),"{\"$type\":\"Object\"}","application/json;charset=UTF-8")
if isLoginRedirect(libraryContent) then
print("EquatePlus: FetchStatements — session expired, got login page.")
return {statements={}}
end
local library = JSON(libraryContent):dictionary()
if not library or not library["documents"] then
print("EquatePlus: documents/library has no 'documents' field — API may have changed.")
if library then for k, _ in pairs(library) do print(" key: " .. tostring(k)) end end
return {statements={}}
end
local pattern = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)"
for k,document in pairs(library["documents"]) do
local statement = {}
local year, month, day, hour, minute, second = document["date"]:match(pattern)
statement.creationDate = os.time({year=year,month=month,day=day})
statement.name = document["description"]
statement.identifier = document["id"]
statement.filename = (document["description"] .. "(" .. MM.localizeDate(statement.creationDate) .. ").pdf"):gsub("/", "-")
if not knownIdentifiers[statement.identifier] then
if debugging then print("Downloading statement: " .. statement.filename) end
statement.pdf = connectWithCSRF("GET", "https://www.equateplus.com/EquatePlusParticipant2/services/statements/download?documentId="..statement.identifier.."&downloadType=inline&source=LIBRARY")
if startsWith(statement.pdf, "{\"$type\":\"TechnicalError\"") then
print("error downloading statement")
else
table.insert(statements, statement)
end
end
end
return {statements=statements}
end
function bugReport(status,err,v)
if not status and reportOnce then
reportOnce=false
print (string.rep('#',25).." 8< please report this bug = '"..err.."' >8 "..string.rep('#',25))
tprint(v)
print (string.rep('#',25).." 8< please report this bug version="..Version.." >8 "..string.rep('#',25))
end
end
function EndSession ()
-- Logout.
connectWithCSRF("GET","https://www.equateplus.com/EquatePlusParticipant2/services/participant/logout")
end
-- SIGNATURE: MCwCFGiSlouFnhu7ankjaIYZx/ZFZ1O+AhQwTaDiI85Bun6E6q3PF/hBlp4sKw==