{
  "openapi": "3.0.3",
  "info": {
    "title": "Flynt MSCOA Integrator API",
    "version": "1.1.0",
    "summary": "Sage 300 People to Munsoft GL export",
    "description": "Retrieves the General Ledger export for the last three month-ends from Sage 300 People (registered Generic GET query `FLYNT_MSCOA`) and returns it as JSON rows, each carrying a ready-made line in the Munsoft comma-delimited layout.\n\n### Authentication flow\n1. `POST /api/login` with the shared team password. A secure, HttpOnly session cookie (`fmi_session`) is set, valid for 8 hours.\n2. Call `POST /api/extract` with that cookie to receive the rows.\n3. `POST /api/logout` clears the session.\n\nAll Sage credentials are held server-side and are never exposed to the caller.\n\n### Row keys\nSage serialises column headings to camel-case with spaces retained and the first word lowercased, for example `Is Debit` becomes `is Debit`. Consumers should match keys case-insensitively and ignore spaces.\n\n### Export layout\nEach row's `csvLine` is a 15-field comma-delimited line in this order: `Date, Description, Reference, Amount, Use Tax, Tax Type, Tax Account, Tax Amount, Project, Function, Item, Funding, Region, Costing, Is Debit`. The integrator writes a header row and CRLF line endings when producing the file.\n\n### Period\nThe extract returns every company rule's payroll for the last three month-ends that have calculated payslips. Choose the month in the Integrator; each export file covers one month for all selected company rules.\n\n### Account segments\nControl and liability accounts are posted as configured. Expense accounts are resolved per employee: Project, Function, Fund, Region and Costing come from the employee's hierarchy assignments (headers 1 to 5), and the Item comes from the GL account link for the definition and GL group. Lines whose segments cannot be resolved are excluded and show up as an imbalance in the Integrator's balance check.",
    "contact": {
      "name": "Flynt (Pty) Ltd",
      "url": "https://flyntza.co.za",
      "email": "craig@flyntza.co.za"
    },
    "x-logo": {
      "url": "https://flyntza.co.za/wp-content/uploads/2025/08/Flynt-Primary-Logo-300x124.webp",
      "altText": "Flynt"
    }
  },
  "servers": [
    {
      "url": "https://flynt-mscoa-integrator.pages.dev",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Obtain, check and clear the session used to call the extract."
    },
    {
      "name": "GL Extract",
      "description": "Fetch the current-period General Ledger export from Sage 300 People."
    }
  ],
  "paths": {
    "/api/login": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "login",
        "summary": "Sign in",
        "description": "Validates the shared team password and issues the `fmi_session` cookie (HttpOnly, Secure, SameSite=Strict, 8 hour TTL).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              },
              "examples": {
                "default": {
                  "value": {
                    "password": "your-team-password"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authenticated. Session cookie set.",
            "headers": {
              "Set-Cookie": {
                "description": "`fmi_session=<token>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=28800`",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "examples": {
                  "default": {
                    "value": {
                      "ok": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Incorrect password.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "ok": false,
                      "error": "Incorrect password"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server not configured (missing `APP_PASSWORD` or `SESSION_SECRET`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "ok": false,
                      "error": "Server not configured"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/session": {
      "get": {
        "tags": [
          "Authentication"
        ],
        "operationId": "getSession",
        "summary": "Check session",
        "description": "Reports whether the caller currently holds a valid session cookie.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Session status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionStatus"
                },
                "examples": {
                  "authenticated": {
                    "value": {
                      "authenticated": true
                    }
                  },
                  "anonymous": {
                    "value": {
                      "authenticated": false
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/logout": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "operationId": "logout",
        "summary": "Sign out",
        "description": "Clears the session cookie.",
        "responses": {
          "200": {
            "description": "Signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                },
                "examples": {
                  "default": {
                    "value": {
                      "ok": true
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/extract": {
      "post": {
        "tags": [
          "GL Extract"
        ],
        "operationId": "fetchExtract",
        "summary": "Fetch the GL export (last three month-ends)",
        "description": "Authenticates to Sage 300 People, invokes `GenericGet/FLYNT_MSCOA`, and returns the rows for the last three month-ends with calculated payslips. Each row carries its period in `date` (ddMMyyyy) and its `company Rule Code`, so a caller can select one month and one or more company rules. Requires a valid session cookie.",
        "security": [
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Rows returned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExtractResult"
                },
                "examples": {
                  "default": {
                    "value": {
                      "ok": true,
                      "count": 2,
                      "fetchedAt": "2026-08-27T09:15:00.000Z",
                      "rows": [
                        {
                          "company Rule Code": "COUNCELLORS",
                          "company Rule": "Councellors",
                          "date": "30062026",
                          "description": "COUNCILLORS PAYROLL",
                          "reference": "",
                          "amount": "28215.60",
                          "use Tax": "N",
                          "tax Type": "",
                          "tax Account": "",
                          "tax Amount": "",
                          "project": "53d432c7-5d06-4d53-a785-f29995840060",
                          "function": "d322a6d8-8a77-4f3e-b409-e49df0b85989",
                          "item": "b5767e3c-4e0b-448c-9411-29efcb0901c9",
                          "funding": "b24ed953-03ae-4467-ba3b-3fc158896032",
                          "region": "5f4d5c9c-e20a-4075-aa3f-9f78211dca90",
                          "costing": "47c7ba65-c270-4a7f-91ba-3842eb629ddf",
                          "is Debit": "N",
                          "csvLine": "30062026,COUNCILLORS PAYROLL,,28215.60,N,,,,53d432c7-5d06-4d53-a785-f29995840060,d322a6d8-8a77-4f3e-b409-e49df0b85989,b5767e3c-4e0b-448c-9411-29efcb0901c9,b24ed953-03ae-4467-ba3b-3fc158896032,5f4d5c9c-e20a-4075-aa3f-9f78211dca90,47c7ba65-c270-4a7f-91ba-3842eb629ddf,N",
                          "employee Code": "C000010",
                          "defCode": "CAR_ALLOWANCE"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated. Sign in via `/api/login` first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "default": {
                    "value": {
                      "ok": false,
                      "error": "Not authenticated"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Server not configured (a required Sage variable is missing).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Upstream Sage error (token or GenericGet). `detail` carries Sage's message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "token": {
                    "value": {
                      "ok": false,
                      "error": "Sage token error",
                      "status": 400,
                      "detail": "{\"error\":\"Invalid API Key\"}"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "fmi_session",
        "description": "Session cookie issued by `POST /api/login`. Browsers and Swagger UI send it automatically once signed in."
      }
    },
    "schemas": {
      "LoginRequest": {
        "type": "object",
        "required": [
          "password"
        ],
        "properties": {
          "password": {
            "type": "string",
            "description": "Shared team password."
          }
        }
      },
      "Ok": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "SessionStatus": {
        "type": "object",
        "properties": {
          "authenticated": {
            "type": "boolean"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string",
            "description": "Short error code/message."
          },
          "status": {
            "type": "integer",
            "description": "Upstream HTTP status, when applicable."
          },
          "detail": {
            "type": "string",
            "description": "Upstream error body (truncated), when applicable."
          }
        }
      },
      "ExtractResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "example": true
          },
          "count": {
            "type": "integer",
            "description": "Number of rows returned.",
            "example": 6447
          },
          "fetchedAt": {
            "type": "string",
            "format": "date-time"
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GLRow"
            }
          }
        }
      },
      "GLRow": {
        "type": "object",
        "description": "One GL posting line (debit or credit side). Keys are as serialised by Sage.",
        "properties": {
          "company Rule Code": {
            "type": "string",
            "example": "COUNCELLORS"
          },
          "company Rule": {
            "type": "string",
            "example": "Councellors"
          },
          "date": {
            "type": "string",
            "description": "Period end, ddMMyyyy.",
            "example": "30062026"
          },
          "description": {
            "type": "string",
            "maxLength": 30,
            "example": "COUNCILLORS PAYROLL"
          },
          "reference": {
            "type": "string",
            "maxLength": 30
          },
          "amount": {
            "type": "string",
            "description": "Unsigned, 13.2 format.",
            "example": "28215.60"
          },
          "use Tax": {
            "type": "string",
            "enum": [
              "N"
            ]
          },
          "tax Type": {
            "type": "string"
          },
          "tax Account": {
            "type": "string"
          },
          "tax Amount": {
            "type": "string"
          },
          "project": {
            "type": "string",
            "maxLength": 39
          },
          "function": {
            "type": "string",
            "maxLength": 39
          },
          "item": {
            "type": "string",
            "maxLength": 39
          },
          "funding": {
            "type": "string",
            "maxLength": 39
          },
          "region": {
            "type": "string",
            "maxLength": 39
          },
          "costing": {
            "type": "string",
            "maxLength": 39
          },
          "is Debit": {
            "type": "string",
            "enum": [
              "Y",
              "N"
            ],
            "description": "Y = debit, N = credit (contra)."
          },
          "csvLine": {
            "type": "string",
            "description": "Ready-to-write Munsoft line, 15 comma-delimited fields."
          },
          "employee Code": {
            "type": "string",
            "example": "C000010"
          },
          "defCode": {
            "type": "string",
            "example": "CAR_ALLOWANCE"
          }
        }
      }
    }
  }
}