#include "tlpp-core.th"
#include "tlpp-rest.th"

/*
Exemplo didatico do site Pragiva. Le e grava na SA2 (fornecedores) do
dicionario padrao. Precisa do servico REST configurado no ambiente.

DUAS CLASSES, de proposito. Heranca do adapter e annotation de verbo nao
convivem na mesma classe: o controller anotado responde a HTTP; a consulta
paginada herda o adapter e nao sabe que HTTP existe. Juntar as duas compila,
mas inverte a separacao que o framework pressupoe.

Arquivo unico com duas classes e recurso raro. Aqui existe para o exemplo
caber num anexo; num projeto real, um arquivo por classe.

Rotas:
  GET    /api/pragiva/v1/fornecedores  -> listagem paginada
  POST   /api/pragiva/v1/fornecedores  -> inclusao pelo ExecAuto
  DELETE /api/pragiva/v1/fornecedores  -> exclusao pelo ExecAuto
*/

namespace pragiva.api.fornecedores

// O CONTROLLER. Nao herda nada: quem responde pelas rotas e ele, e o objeto
// da requisicao chega injetado pelo runtime da annotation.
Class FornecedoresApi

    Public Method New()

    @Get("/api/pragiva/v1/fornecedores")
    Public Method GetFornecedores()

    @Post("/api/pragiva/v1/fornecedores")
    Public Method PostFornecedores()

    @Delete("/api/pragiva/v1/fornecedores")
    Public Method DeleteFornecedores()

    Private Method Responder(nCodigo, cCorpo)
    Private Method ResponderErro(nCodigo, cMensagem)
    Private Method ErroDoExecAuto()

EndClass

// A CONSULTA. Esta sim herda o adapter, e nao tem annotation nenhuma.
Class FornecedoresData From FWAdapterBaseV2
    Public Method New()
    Public Method BuscarPagina(nPagina, nPorPag)
EndClass

Method New() Class FornecedoresApi
Return Self

// Listagem paginada. O controller nao sabe paginar: instancia a classe de
// consulta e devolve o JSON que ela montou.
//
// "::" e apelido de "Self:", entao serve para os metodos DESTA classe. O
// objeto da requisicao nao e da classe: e injetado, e so atende por "oRest:".
Method GetFornecedores() Class FornecedoresApi

    Local oDados  as Object
    Local jQuery  as Json
    Local nPagina as Numeric
    Local nPorPag as Numeric

    try
        jQuery := oRest:getQueryRequest()

        // Uma chamada sem query string nenhuma e o caso mais comum aqui, e
        // nessa situacao o retorno pode nao ser objeto -- por isso o teste
        // contra Nil antes de qualquer metodo.
        // getQueryRequest() devolve Json; GetJsonText() de chave ausente
        // volta a string literal 'null', nao Nil -- por isso o teste e
        // contra essa string, com Val() convertendo para numero.
        nPagina := 1
        nPorPag := 10
        If jQuery != Nil .And. jQuery:GetJsonText("page") != "null"
            nPagina := Val(jQuery:GetJsonText("page"))
        EndIf
        If jQuery != Nil .And. jQuery:GetJsonText("pageSize") != "null"
            nPorPag := Val(jQuery:GetJsonText("pageSize"))
        EndIf

        oDados := FornecedoresData():New()

        If oDados:BuscarPagina(nPagina, nPorPag)
            // O envelope tem items, hasNext e remainingRecords -- e esse
            // formato que o componente de lista do PO UI espera para
            // paginar sozinho, sem tratamento a mais no front.
            ::Responder(200, oDados:GetJSONResponse())
        Else
            ::ResponderErro(oDados:GetCode(), oDados:GetMessage())
        EndIf
    catch oErro
        ::ResponderErro(500, oErro:Description)
    endtry

Return

// Inclusao pelo ExecAuto, operacao 3. O que derruba quem migra do
// WSRESTFUL: getBodyRequest() devolve Character, nao Json pronto.
Method PostFornecedores() Class FornecedoresApi

    Local jCorpo as Json
    Local aDados as Array
    Local cErro  as Character

    // lMsErroAuto e Private, nao Local: o ExecAuto grava o resultado nela
    // por escopo dinamico, e uma declaracao Local fica invisivel para quem
    // esta mais fundo na pilha. Vai ANTES do try, junto das demais
    // declaracoes -- Private dentro de try nao e forma reconhecida.
    Private lMsErroAuto := .F.

    try
        jCorpo := JsonObject():New()

        // FromJson DEVOLVE o erro de parse, e descartar esse retorno e o
        // buraco silencioso desta rota: com body malformado o objeto fica
        // vazio, cada campo vira Nil, o ExecAuto nao acusa nada -- e a API
        // responde 201 "incluido" sem ter gravado uma linha.
        cErro := jCorpo:FromJson(oRest:getBodyRequest())
        If !Empty(cErro)
            ::ResponderErro(400, "corpo da requisicao nao e um JSON valido")
            Return
        EndIf

        If Empty(jCorpo["codigo"]) .Or. Empty(jCorpo["loja"]) .Or. ;
           Empty(jCorpo["nome"])
            ::ResponderErro(400, "informe codigo, loja e nome")
            Return
        EndIf

        aDados := {}
        Aadd(aDados, {"A2_FILIAL", xFilial("SA2")})
        Aadd(aDados, {"A2_COD"   , jCorpo["codigo"]})
        Aadd(aDados, {"A2_LOJA"  , jCorpo["loja"]})
        Aadd(aDados, {"A2_NOME"  , jCorpo["nome"]})
        Aadd(aDados, {"A2_NREDUZ", jCorpo["nomeReduzido"]})
        Aadd(aDados, {"A2_CGC"   , jCorpo["cnpjCpf"]})

        MsExecAuto({|x, y| MATA020(x, y)}, aDados, 3)

        If lMsErroAuto
            ::ResponderErro(400, ::ErroDoExecAuto())
        Else
            ::Responder(201, '{"mensagem":"fornecedor incluido"}')
        EndIf
    catch oErro
        ::ResponderErro(500, oErro:Description)
    endtry

Return

// Exclusao pelo ExecAuto, operacao 5. Quem se posiciona no registro e o
// proprio MATA020: para a operacao 5 o framework monta a chave a partir do
// ARRAY recebido e faz o seek sozinho. O DbSeek local existe por outro
// motivo -- responder 404 antes de entrar no ExecAuto.
Method DeleteFornecedores() Class FornecedoresApi

    Local jQuery  as Json
    Local cCodigo as Character
    Local cLoja   as Character
    Local aDados  as Array
    Local aArea   as Array

    Private lMsErroAuto := .F.

    // A thread REST e reaproveitada entre requisicoes: a area de trabalho
    // que este metodo posiciona sobrevive a ele. Guardar e restaurar impede
    // que a proxima requisicao receba a SA2 parada onde esta parou.
    aArea := GetArea()

    try
        jQuery  := oRest:getQueryRequest()
        cCodigo := jQuery:GetJsonText("codigo")
        cLoja   := jQuery:GetJsonText("loja")

        // Parametro ausente vira a string 'null'. Sem esta guarda, a
        // concatenacao da chave estoura e o cliente recebe 500 onde 400
        // descreve o problema de verdade.
        If Empty(cCodigo) .Or. cCodigo == "null" .Or. ;
           Empty(cLoja) .Or. cLoja == "null"
            ::ResponderErro(400, "informe codigo e loja")
            Return
        EndIf

        // PadR NAO e detalhe de estilo aqui: DbSeek casa por PREFIXO. Um
        // codigo mais curto que o campo para no primeiro registro que
        // COMECA com aquele valor -- e, numa exclusao, isso apaga o
        // fornecedor errado sem nenhum aviso.
        cCodigo := PadR(cCodigo, TamSX3("A2_COD")[1])
        cLoja   := PadR(cLoja, TamSX3("A2_LOJA")[1])

        DbSelectArea("SA2")
        SA2->(DbSetOrder(1)) // A2_FILIAL + A2_COD + A2_LOJA
        SA2->(DbSeek(xFilial("SA2") + cCodigo + cLoja))

        If !SA2->(Found())
            ::ResponderErro(404, "fornecedor nao encontrado")
            Return
        EndIf

        aDados := {}
        Aadd(aDados, {"A2_FILIAL", xFilial("SA2")})
        Aadd(aDados, {"A2_COD"   , cCodigo})
        Aadd(aDados, {"A2_LOJA"  , cLoja})

        MsExecAuto({|x, y| MATA020(x, y)}, aDados, 5)

        If lMsErroAuto
            ::ResponderErro(400, ::ErroDoExecAuto())
        Else
            ::Responder(200, '{"mensagem":"fornecedor excluido"}')
        EndIf
    catch oErro
        ::ResponderErro(500, oErro:Description)
    endtry

    RestArea(aArea)

Return

// Um lugar so para o par setStatusCode + setResponse.
Method Responder(nCodigo, cCorpo) Class FornecedoresApi
    oRest:setKeyHeaderResponse("Content-Type", "application/json")
    oRest:setStatusCode(nCodigo)
    oRest:setResponse(cCorpo)
Return

// O corpo do erro vai montado como OBJETO e serializado com ToJson: a
// mensagem de um erro real carrega aspas duplas, barra invertida e quebra
// de linha, e qualquer um dos tres invalida um JSON concatenado a mao.
Method ResponderErro(nCodigo, cMensagem) Class FornecedoresApi
    Local jErro := JsonObject():New() as Json
    jErro["erro"] := cMensagem
    ::Responder(nCodigo, jErro:ToJson())
Return

// O log do ExecAuto vem como array de linhas.
Method ErroDoExecAuto() Class FornecedoresApi
    Local aLog   := GetAutoGRLog() as Array
    Local cTexto := "" as Character
    Local nLinha := 0 as Numeric
    For nLinha := 1 To Len(aLog)
        cTexto += aLog[nLinha] + " "
    Next nLinha
Return AllTrim(cTexto)

// ---------------------------------------------------------------------
// A classe de consulta. Herda o adapter e NAO tem annotation -- e assim
// que as duas responsabilidades ficam separadas.
// ---------------------------------------------------------------------

// _Super com underscore: e a unica grafia que o compilador reconhece.
Method New() Class FornecedoresData
    _Super:New("GET", .T.)
Return Self

Method BuscarPagina(nPagina, nPorPag) Class FornecedoresData

    Local cQuery as Character
    Local cWhere as Character

    // Os coringas sao #QueryFields# e #QueryWhere#, entre cerquilhas --
    // %QUERY_FIELDS% e %QUERY_WHERE% nao existem no framework.
    cQuery := "SELECT #QueryFields# FROM " + RetSqlName("SA2") + " SA2 "
    cQuery += " WHERE #QueryWhere# "

    cWhere := " SA2.A2_FILIAL = " + ValToSQL(xFilial("SA2")) + ;
              " AND SA2.D_E_L_E_T_ = ' ' "

    // AddMapFields, no plural -- SetFieldMap e AddMapField (singular) nao
    // existem. O 5o parametro e sempre {nome, tipo, tamanho, decimais};
    // TamSX3() devolve {tamanho, decimais, tipo}, entao o tamanho e o [1].
    ::AddMapFields("codigo"      , "A2_COD"   , .T., .T., {"A2_COD"   , "C", TamSX3("A2_COD")[1]   , 0})
    ::AddMapFields("loja"        , "A2_LOJA"  , .T., .T., {"A2_LOJA"  , "C", TamSX3("A2_LOJA")[1]  , 0})
    ::AddMapFields("nome"        , "A2_NOME"  , .T., .F., {"A2_NOME"  , "C", TamSX3("A2_NOME")[1]  , 0})
    ::AddMapFields("nomeReduzido", "A2_NREDUZ", .T., .F., {"A2_NREDUZ", "C", TamSX3("A2_NREDUZ")[1], 0})
    ::AddMapFields("cnpjCpf"     , "A2_CGC"   , .T., .F., {"A2_CGC"   , "C", TamSX3("A2_CGC")[1]   , 0})

    ::SetQuery(cQuery)
    ::SetWhere(cWhere)
    ::SetOrder("A2_FILIAL, A2_COD, A2_LOJA")
    ::SetPage(nPagina)
    ::SetPageSize(nPorPag)

    If !::Execute()
        Return .F.
    EndIf

    // A sequencia e Execute() -> FillGetResponse() -> GetJSONResponse().
    // GetResponse() nao existe nesse fluxo. FillGetResponse() nao recebe
    // parametro nenhum.
    ::FillGetResponse()

Return .T.
