Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f89fd7e
refactor(lgpd): tira do customers as colunas de dado pessoal sem uso
sousa7tz Oct 6, 2026
72fbd49
feat(cadastro): aceita cnpj alfanumerico
sousa7tz Oct 6, 2026
13fff43
feat(balcao): estorno de resgate pelo extrato
sousa7tz Oct 6, 2026
c7ec9ee
feat(balcao): corrigir nome e trocar o telefone do cliente
sousa7tz Oct 6, 2026
0f6cdfe
feat(acessibilidade): estilos em classe, botao carregando e campo com…
sousa7tz Oct 6, 2026
dd37bd4
feat(seguranca): csp completa sem nada inline e hsts em https
sousa7tz Oct 6, 2026
3ee1815
feat(relatorios): filtro por periodo e exportacao em csv
sousa7tz Oct 6, 2026
7a1cb79
feat(operacao): rota de saude sem sessao que confere o banco
sousa7tz Oct 6, 2026
cec19fb
chore(qualidade): phpstan nivel 5 com baseline e composer analyse
sousa7tz Oct 6, 2026
5c0993b
feat(email): interface de envio com driver de log e sem provedor por …
sousa7tz Oct 6, 2026
8f76199
feat(senha): recuperacao de senha por e-mail
sousa7tz Oct 6, 2026
66b36c6
feat(conta): confirmacao do e-mail do lojista
sousa7tz Oct 6, 2026
7a5510b
fix(phpstan): tira da baseline o codigo inalcancavel do esqueci a senha
sousa7tz Oct 6, 2026
ee200ba
feat(admin): painel administrativo com login proprio
sousa7tz Oct 6, 2026
51731a7
docs(adr): proposta de atendentes com login proprio
sousa7tz Oct 6, 2026
3a401a7
Merge pull request #39 from stargolab/feat/admin
andynnnfw Oct 9, 2026
8d5e5fc
Merge pull request #38 from stargolab/feat/email-conta
andynnnfw Oct 9, 2026
8d7283b
Merge pull request #37 from stargolab/chore/operacao-qualidade
andynnnfw Oct 9, 2026
5468e97
Merge pull request #36 from stargolab/feat/csp-relatorios
andynnnfw Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ BACKUP_DIR=
BACKUP_KEEP_DAYS=
MYSQLDUMP_BIN=

# e-mail transacional (recuperacao de senha, confirmacao de e-mail). Vazio = nenhum e-mail sai e o log avisa
# (o provedor de producao ainda nao foi escolhido). Em desenvolvimento use MAIL_DRIVER=log: cada e-mail vira uma
# linha json em MAIL_LOG_FILE (vazio = storage/mail.log), com os links. Nunca use "log" em producao.
MAIL_DRIVER=
MAIL_LOG_FILE=

# so para o docker compose: senha do root do MySQL do container e porta publicada no host (vazio = 8080).
# No compose, DB_PASS acima e a senha do usuario "fidelityx" do banco e nao pode ficar vazia.
DB_ROOT_PASS=
Expand Down
24 changes: 19 additions & 5 deletions CLAUDE.md

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions bin/create-admin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
// cria um administrador do FidelityX (task 30), que entra em /index.php?url=admin/login.
//
// php bin/create-admin.php email@dominio.com "Nome"
//
// a senha e pedida no terminal (ou lida da entrada: echo "senha" | php bin/create-admin.php ...),
// nunca vai como argumento (ficaria no historico do shell).
// usa o banco do .env / variaveis de ambiente (DB_NAME).

require __DIR__ . '/../vendor/autoload.php';

use App\Database;
use App\Models\AdminModel;
use App\Support\Env;

if (PHP_SAPI !== 'cli') {
exit(1);
}

Env::load(dirname(__DIR__));
date_default_timezone_set(app_timezone());

$email = trim((string)($argv[1] ?? ''));
$name = trim((string)($argv[2] ?? ''));
if ($email === '' || $name === '') {
fwrite(STDERR, "uso: php bin/create-admin.php email@dominio.com \"Nome\"\n");
exit(1);
}

fwrite(STDOUT, 'senha: ');
$password = rtrim((string)fgets(STDIN), "\r\n");

$problem = AdminModel::problem($email, $name, $password);
if ($problem !== null) {
fwrite(STDERR, "erro: $problem\n");
exit(1);
}

try {
if (Env::missing() !== []) {
throw new RuntimeException('variaveis obrigatorias sem valor: ' . implode(', ', Env::missing()));
}
$id = (new AdminModel(Database::connect()))->create($email, $name, $password);
fwrite(STDOUT, "\nadmin criado (id $id). entre em /index.php?url=admin/login\n");
} catch (PDOException $e) {
fwrite(STDERR, (($e->errorInfo[1] ?? null) === 1062 ? 'erro: ja existe admin com esse e-mail' : 'erro no banco: ' . $e->getMessage()) . "\n");
exit(1);
} catch (Throwable $e) {
fwrite(STDERR, 'erro: ' . $e->getMessage() . "\n");
exit(1);
}
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@
}
},
"scripts": {
"test": "phpunit"
"test": "phpunit",
"analyse": "phpstan analyse --memory-limit=1G --no-progress"
},
"config": {
"platform": {
"php": "8.1.0"
}
},
"require-dev": {
"phpunit/phpunit": "^10.5"
"phpunit/phpunit": "^10.5",
"phpstan/phpstan": "^2.1"
}
}
66 changes: 65 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions database/migrations/007_customers_so_telefone.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- fidelityx - migration 007 (cliente e so o telefone, task 49)
-- para bancos criados antes desta versao do schema.sql. rodar uma unica vez:
-- php bin/migrate.php
-- (ou, a mao: mysql -u root -p fidelityx < database/migrations/007_customers_so_telefone.sql)

USE fidelityx;

-- cpf, email, birth_date e gender estavam "para o futuro" e nunca foram usados: o cliente e so o telefone
-- (task 11, docs/adr/002) e o nome fica no cartao de cada loja. coluna vazia de dado pessoal convida a
-- guardar dado sem base legal, entao sai. o indice unico do cpf e o do email saem junto.
ALTER TABLE customers
DROP KEY uq_customers_cpf,
DROP KEY idx_customers_email,
DROP COLUMN cpf,
DROP COLUMN email,
DROP COLUMN birth_date,
DROP COLUMN gender;
27 changes: 27 additions & 0 deletions database/migrations/008_password_resets.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-- fidelityx - migration 008 (recuperacao de senha por e-mail, task 21)
-- para bancos criados antes desta versao do schema.sql. rodar uma unica vez:
-- php bin/migrate.php
-- (ou, a mao: mysql -u root -p fidelityx < database/migrations/008_password_resets.sql)

USE fidelityx;

-- link de redefinicao de senha: o token vai so no e-mail; aqui fica o hash sha-256 dele
-- (quem ler o banco nao consegue usar). vale 1 hora e uma vez so.
CREATE TABLE IF NOT EXISTS password_resets (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

merchant_id BIGINT UNSIGNED NOT NULL,
token_hash CHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP NULL, -- usado ou cancelado (pedido novo ou senha trocada)

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

UNIQUE KEY uq_password_resets_token (token_hash),
KEY idx_password_resets_merchant (merchant_id),

CONSTRAINT fk_password_resets_merchant
FOREIGN KEY (merchant_id) REFERENCES merchants(id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
32 changes: 32 additions & 0 deletions database/migrations/009_email_verifications.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- fidelityx - migration 009 (confirmacao do e-mail do lojista, task 50)
-- para bancos criados antes desta versao do schema.sql. rodar uma unica vez:
-- php bin/migrate.php
-- (ou, a mao: mysql -u root -p fidelityx < database/migrations/009_email_verifications.sql)

USE fidelityx;

-- conta nova so usa o painel depois de confirmar o e-mail (NULL = ainda nao confirmou)
ALTER TABLE merchants ADD COLUMN email_verified_at TIMESTAMP NULL AFTER email;

-- contas que ja existiam contam como confirmadas: nao da pra travar quem ja usa o sistema
UPDATE merchants SET email_verified_at = created_at WHERE email_verified_at IS NULL;

-- link de confirmacao: como o de nova senha, so o hash do token fica aqui. vale 24 horas e uma vez.
CREATE TABLE IF NOT EXISTS email_verifications (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

merchant_id BIGINT UNSIGNED NOT NULL,
token_hash CHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP NULL, -- usado ou cancelado (reenvio)

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

UNIQUE KEY uq_email_verifications_token (token_hash),
KEY idx_email_verifications_merchant (merchant_id),

CONSTRAINT fk_email_verifications_merchant
FOREIGN KEY (merchant_id) REFERENCES merchants(id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
21 changes: 21 additions & 0 deletions database/migrations/010_admins.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- fidelityx - migration 010 (painel administrativo, task 30)
-- para bancos criados antes desta versao do schema.sql. rodar uma unica vez:
-- php bin/migrate.php
-- (ou, a mao: mysql -u root -p fidelityx < database/migrations/010_admins.sql)

USE fidelityx;

-- administradores do FidelityX (nao sao lojistas): login proprio em admin/login, conta criada
-- pela linha de comando (php bin/create-admin.php). veem as lojas, nunca os clientes delas.
CREATE TABLE IF NOT EXISTS admins (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

email VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

UNIQUE KEY uq_admins_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
84 changes: 72 additions & 12 deletions database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ CREATE TABLE IF NOT EXISTS merchants (
owner_name VARCHAR(255) NOT NULL,
store_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
email_verified_at TIMESTAMP NULL, -- conta nova so usa o painel depois de confirmar o e-mail (task 50)
phone VARCHAR(30) NOT NULL,
category VARCHAR(30) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
Expand Down Expand Up @@ -57,18 +58,10 @@ CREATE TABLE IF NOT EXISTS customers (

phone VARCHAR(30) NOT NULL,

-- Future-proofing (NULL)
cpf VARCHAR(11) NULL,
email VARCHAR(255) NULL,
birth_date DATE NULL,
gender ENUM('male', 'female', 'other') NULL,

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

UNIQUE KEY uq_customers_phone (phone),
UNIQUE KEY uq_customers_cpf (cpf),
KEY idx_customers_email (email)
UNIQUE KEY uq_customers_phone (phone)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- =========================
Expand Down Expand Up @@ -140,11 +133,11 @@ CREATE TABLE IF NOT EXISTS points_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

card_id BIGINT UNSIGNED NOT NULL,
type ENUM('earn', 'redeem', 'reversal') NOT NULL, -- reversal = estorno de um earn
type ENUM('earn', 'redeem', 'reversal') NOT NULL, -- reversal = estorno de um earn ou de um redeem
quantity INT NOT NULL,
description VARCHAR(255) NOT NULL,
reward_id BIGINT UNSIGNED NULL, -- preenchido quando type = 'redeem'
reverses_id BIGINT UNSIGNED NULL, -- preenchido quando type = 'reversal': o lancamento estornado
reverses_id BIGINT UNSIGNED NULL, -- preenchido quando type = 'reversal': o ganho ou resgate estornado

-- para futuras implementações (NULL)
responsible_user VARCHAR(255) NULL,
Expand Down Expand Up @@ -190,6 +183,69 @@ CREATE TABLE IF NOT EXISTS rate_limit_hits (
KEY idx_rate_limit_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- =========================
-- password_resets // links de redefinicao de senha (task 21)
-- =========================
-- o token vai so no e-mail; aqui fica o hash sha-256 dele. vale 1 hora e uma vez so.
CREATE TABLE IF NOT EXISTS password_resets (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

merchant_id BIGINT UNSIGNED NOT NULL,
token_hash CHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP NULL, -- usado ou cancelado (pedido novo ou senha trocada)

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

UNIQUE KEY uq_password_resets_token (token_hash),
KEY idx_password_resets_merchant (merchant_id),

CONSTRAINT fk_password_resets_merchant
FOREIGN KEY (merchant_id) REFERENCES merchants(id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- =========================
-- email_verifications // links de confirmacao do e-mail do lojista (task 50)
-- =========================
-- como o de nova senha: so o hash do token fica aqui. vale 24 horas e uma vez.
CREATE TABLE IF NOT EXISTS email_verifications (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

merchant_id BIGINT UNSIGNED NOT NULL,
token_hash CHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP NULL, -- usado ou cancelado (reenvio)

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,

UNIQUE KEY uq_email_verifications_token (token_hash),
KEY idx_email_verifications_merchant (merchant_id),

CONSTRAINT fk_email_verifications_merchant
FOREIGN KEY (merchant_id) REFERENCES merchants(id)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- =========================
-- admins // administradores do FidelityX (task 30)
-- =========================
-- login proprio em admin/login, conta criada por php bin/create-admin.php. veem as lojas, nunca os clientes.
CREATE TABLE IF NOT EXISTS admins (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,

email VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,

created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

UNIQUE KEY uq_admins_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- =========================
-- schema_migrations // quais migrations de database/migrations ja rodaram neste banco
-- =========================
Expand All @@ -207,4 +263,8 @@ INSERT IGNORE INTO schema_migrations (version) VALUES
('003_lgpd'),
('004_estorno'),
('005_regra_pontos'),
('006_points_log_extrato');
('006_points_log_extrato'),
('007_customers_so_telefone'),
('008_password_resets'),
('009_email_verifications'),
('010_admins');
Loading
Loading