gerencianet / gn-api-sdk-typescript

SDK em TypeScript integrada a API Gerencianet. Esta SDK está preparada para integração à API Pix e API Boletos da Gerencianet, que lhe permite realizar o gerenciamento de cobranças Pix com QR Code e Pix Copia e Cola, boleto/Bolix, carnê, cartão de crédito e muito mais.
14 stars 5 forks source link

[Sugestão] Adicionar tipagem e validação de dados nos métodos da SDK #8

Open DenisBessa opened 1 year ago

DenisBessa commented 1 year ago

Uma sugestão que melhoraria muito a SDK: adicionar tipos aos métodos, de modo que o desenvolvedor saiba quais valores são aceitos para cada parâmetro da SDK e quais valores a API vai retornar. Isso melhoria muito a DevX, porque o desenvolvedor não precisaria ficar lendo a documentação pra saber quai(s) campos são obrigatórios, e o próprio ambiente de desenvolvimento pode retornar se o objeto estiver incompleto ou inválido, por exemplo.

Estou falando de algo mais ou menos assim:

type BolixCreate = {
    payment: {
        banking_billet: {
            message?: string | undefined;
            configurations?: {
                fine: number;
                interest: number;
            } | undefined;
            expire_at: string;
            customer: {
                name: string | null;
                juridical_person: {
                    ...;
                };
                cpf: string | null;
                email: string;
                phone_number: string;
                address: {
                    ...;
                };
            };
        };
    };
    items: {
        ...;
    }[];
}

Daí a SDK poderia exigir este tipo no parâmetro body dos método que cria o Bolix, por exemplo.

Uma outra abordagem, mais sofisticada, seria adicionar uma biblioteca de validação de dados à SDK, como por exemplo o Zod.

Isso permitiria ao aplicativo validar os dados em tempo de execução, antes do envio para a Gerencianet. Desta forma, por exemplo, caso o usuário insira um CNPJ inválido, a própria SDK validaria este campo, sem necessidade de enviar os dados inválidos para a API só pra receber uma resposta de erro.

O Zod permite a validação de tipos em runtime, o que estende a segurança de tipo da aplicação, já que o Typescript faz a validação somente em build time.

Eu mesmo montei a validação "por tentativa e erro" aqui, mas sinto que está incompleto. Se a SDK fizesse isso, seria bem legal.

export const BolixCreateSchema = z.object({
  items: z.array(
    z.object({
      name: z.string().max(255),
      value: z.number().int().min(500),
      amount: z.number().int(),
    })
  ),
  payment: z.object({
    banking_billet: z.object({
      customer: z.object({
        juridical_person: z.object({
          corporate_name: z.string().max(255),
          cnpj: z.string().refine((s) => isCNPJ(s), { message: "CNPJ inválido" }),
        }),
        name: z.string().max(255).nullable(),
        cpf: z
          .string()
          .refine((s) => isCPF(s), { message: "CPF inválido" })
          .nullable(),
        email: z.string().email(),
        phone_number: z.string(),
        address: z.object({
          street: z.string(),
          number: z.string(),
          neighborhood: z.string(),
          zipcode: z.string(),
          city: z.string(),
          complement: z.string(),
          state: z.string(),
        }),
      }),
      expire_at: commonDateToString.refine((s) => moment(s).isAfter(moment()), { message: "Data de vencimento deve ser após a data atual" }),
      configurations: z
        .object({
          fine: z.number(),
          interest: z.number(),
        })
        .optional(),
      message: z.string().optional(),
    }),
  }),
});

Caso os mantenedores gostem da ideia, eu mesmo posso fazer uma PR implementando as sugestões.

brunodmn commented 1 year ago

Estou usando com nestjs e nem os métodos estão aparecendo quando crio a instância...não faz sentido fazer uma lib em typescript e não ser type safe. Deveria fazer direito, ou remover e manter apenas a lib node, que isso é propaganda enganosa.

DenisBessa commented 1 year ago

Já passei por isso, tá aí:

Schema do zod para notificações via webhook:

const BolixNotifications = z.object({
  status: z.string(),
  data: z.array(
    z.object({
      id: z.number(),
      type: z.string(),
      custom_id: z.null(),
      status: z.object({ current: z.string(), previous: z.string().nullable() }),
      identifiers: z.object({ charge_id: z.number() }),
      created_at: z.string(),
    })
  ),
});

E para a API do bolix:

export const BolixCreateSchema = z.object({
  items: z.array(
    z.object({
      name: z.string().max(255, { message: "Deve ter no máximo 255 caracteres" }),
      tempValue: z.number(),
      value: z.number().int().min(500, { message: "Deve ser no mínimo R$ 5,00" }),
      amount: z.number().int().min(1, { message: "Deve ser no mínimo 1" }),
    })
  ),
  payment: z.object({
    banking_billet: z.object({
      customer: z.object({
        juridical_person: z.object({
          corporate_name: z.string().max(255, { message: "Deve ter no máximo 255 caracteres" }).optional(),
          cnpj: z
            .string()
            .refine((s) => isCNPJ(s), { message: "CNPJ inválido" })
            .optional(),
        }),
        name: z.string().max(255).optional(),
        cpf: z
          .string()
          .refine((s) => isCPF(s), { message: "CPF inválido" })
          .optional(),
        email: z.string().email(),

        // this format: .
        phone_number: z.string().regex(/^[1-9]{2}9?[0-9]{8}$/, { message: "Telefone inválido" }),
        address: z.object({
          street: z.string(),
          number: z.string(),
          neighborhood: z.string(),
          zipcode: z.string().refine((s) => isCEP(s), { message: "CEP inválido" }),
          city: z.string(),
          complement: z.string().optional(),
          state: z.string(),
        }),
      }),
      expire_at: z.string().refine((s) => dayjs(s, "YYYY-MM-DD").isAfter(dayjs()), { message: "Deve ser após a data atual" }),
      configurations: z
        .object({
          fine: z.number(),
          interest: z.number(),
        })
        .optional(),
      message: z.string().optional(),
    }),
  }),
  metadata: z.object({
    notification_url: z.literal("[sua url]"),
  }),
  tempCpfCnpj: z.string().refine((s) => isCPFOrCNPJ(s), { message: "CPF/CNPJ inválido" }),
  tempName: z.string().max(255, { message: "Deve ter no máximo 255 caracteres" }),
  tempExpireAt: z.string().refine((s) => dayjs(s, "DD/MM/YYYY").isAfter(dayjs()), { message: "Deve ser após a data atual" }),
});