///| CSV adapters for LedgerWeave's documented import schemas.
///| Quoted commas and doubled quotes are supported; malformed values become records
///|
/// that the normal validation stage reports rather than causing partial mutation.
pub fn parse_csv(text : String) -> Array[Array[String]] {
let rows : Array[Array[String]] = []
for raw in text.split("\n") {
let line = raw.to_owned().trim().to_owned()
if line != "" {
rows.push(parse_csv_line(line))
}
}
rows
}
///|
fn parse_csv_line(line : String) -> Array[String] {
let cells : Array[String] = []
let chars = line.to_array()
let mut field = ""
let mut quoted = false
let mut index = 0
while index < chars.length() {
let char = chars[index]
if char == '"' {
if quoted && index + 1 < chars.length() && chars[index + 1] == '"' {
field = field + "\""
index = index + 1
} else {
quoted = !quoted
}
} else if char == ',' && !quoted {
cells.push(field.trim().to_owned())
field = ""
} else {
field = field + char.to_string()
}
index = index + 1
}
cells.push(field.trim().to_owned())
cells
}
///|
fn column_index(headers : Array[String], name : String) -> Int {
for index = 0; index < headers.length(); index = index + 1 {
if headers[index] == name {
return index
}
}
-1
}
///|
fn field(row : Array[String], index : Int) -> String {
if index < 0 || index >= row.length() {
""
} else {
row[index]
}
}
///|
fn parse_int(text : String) -> Int {
let mut value = 0
let mut seen = false
for char in text.trim().to_owned().to_array() {
let code = char.to_int()
if code >= 48 && code <= 57 {
value = value * 10 + code - 48
seen = true
} else {
return 0
}
}
if seen {
value
} else {
0
}
}
///|
fn order_status(text : String) -> OrderStatus {
match text {
"paid" => Paid
"cancelled" => Cancelled
"refunded" => Refunded
_ => Pending
}
}
///|
fn payment_status(text : String) -> PaymentStatus {
match text {
"voided" => Voided
"failed" => Failed
_ => Captured
}
}
///|
fn direction(text : String) -> AdjustmentDirection {
match text {
"debit" => Debit
_ => Credit
}
}
///|
pub fn parse_orders_csv(text : String) -> Array[OrderRecord] {
let rows = parse_csv(text)
let records : Array[OrderRecord] = []
if rows.is_empty() {
return records
}
let headers = rows[0]
let id = column_index(headers, "id")
let account = column_index(headers, "account_id")
let currency = column_index(headers, "currency")
let amount = column_index(headers, "amount_minor")
let created = column_index(headers, "created_day")
let status = column_index(headers, "status")
for index = 1; index < rows.length(); index = index + 1 {
let row = rows[index]
records.push({
id: field(row, id),
account_id: field(row, account),
currency: field(row, currency),
amount_minor: parse_int(field(row, amount)),
created_day: parse_int(field(row, created)),
status: order_status(field(row, status)),
})
}
records
}
///|
pub fn parse_payments_csv(text : String) -> Array[PaymentRecord] {
let rows = parse_csv(text)
let records : Array[PaymentRecord] = []
if rows.is_empty() {
return records
}
let headers = rows[0]
let id = column_index(headers, "id")
let order_id = column_index(headers, "order_id")
let currency = column_index(headers, "currency")
let amount = column_index(headers, "amount_minor")
let received = column_index(headers, "received_day")
let status = column_index(headers, "status")
for index = 1; index < rows.length(); index = index + 1 {
let row = rows[index]
records.push({
id: field(row, id),
order_id: field(row, order_id),
currency: field(row, currency),
amount_minor: parse_int(field(row, amount)),
received_day: parse_int(field(row, received)),
status: payment_status(field(row, status)),
})
}
records
}
///|
pub fn parse_refunds_csv(text : String) -> Array[RefundRecord] {
let rows = parse_csv(text)
let records : Array[RefundRecord] = []
if rows.is_empty() {
return records
}
let headers = rows[0]
let id = column_index(headers, "id")
let order_id = column_index(headers, "order_id")
let currency = column_index(headers, "currency")
let amount = column_index(headers, "amount_minor")
let refunded = column_index(headers, "refunded_day")
let reason = column_index(headers, "reason")
for index = 1; index < rows.length(); index = index + 1 {
let row = rows[index]
records.push({
id: field(row, id),
order_id: field(row, order_id),
currency: field(row, currency),
amount_minor: parse_int(field(row, amount)),
refunded_day: parse_int(field(row, refunded)),
reason: field(row, reason),
})
}
records
}
///|
pub fn parse_adjustments_csv(text : String) -> Array[AdjustmentRecord] {
let rows = parse_csv(text)
let records : Array[AdjustmentRecord] = []
if rows.is_empty() {
return records
}
let headers = rows[0]
let id = column_index(headers, "id")
let order_id = column_index(headers, "order_id")
let currency = column_index(headers, "currency")
let amount = column_index(headers, "amount_minor")
let day = column_index(headers, "day")
let adjustment_direction = column_index(headers, "direction")
let reason = column_index(headers, "reason")
for index = 1; index < rows.length(); index = index + 1 {
let row = rows[index]
records.push({
id: field(row, id),
order_id: field(row, order_id),
currency: field(row, currency),
amount_minor: parse_int(field(row, amount)),
day: parse_int(field(row, day)),
direction: direction(field(row, adjustment_direction)),
reason: field(row, reason),
})
}
records
}
///|
pub fn parse_ledger_csv(
orders : String,
payments : String,
refunds : String,
adjustments : String,
) -> LedgerInput {
{
orders: parse_orders_csv(orders),
payments: parse_payments_csv(payments),
refunds: parse_refunds_csv(refunds),
adjustments: parse_adjustments_csv(adjustments),
}
}
///|
/// Parses a non-negative day number for command adapters. Invalid input becomes zero.
pub fn parse_day(text : String) -> Int {
parse_int(text)
}