diff --git a/Makefile b/Makefile index 3c2832a..061153d 100644 --- a/Makefile +++ b/Makefile @@ -6,13 +6,20 @@ TARGET ?= local ENVIRONMENT ?= local ROWS ?= 1000 -ifeq ($(ENVIRONMENT),prod) - SUFIX = db +### So o ambiente "qa" usa sufixo -- local e prod usam o nome puro do +### banco (coredb/authdb), cada um no seu proprio host. Confirmado contra +### os secrets reais do Infisical (infra-platform/scripts/extract-env.ps1). +ifeq ($(ENVIRONMENT),qa) + SUFIX = qa else - SUFIX = dbqa + SUFIX = endif -DATABASE_URI = postgresql://$(USER):$(PASSWORD)@$(HOST):$(PORT)/$(TARGET)$(SUFIX) +DB_NAME = $(TARGET)db$(SUFIX) +DATABASE_URI = postgresql://$(USER):$(PASSWORD)@$(HOST):$(PORT)/$(DB_NAME) +### reset precisa de uma conexao de manutencao -- nao da pra DROP DATABASE +### estando conectado nele mesmo. +MAINT_URI = postgresql://$(USER):$(PASSWORD)@$(HOST):$(PORT)/postgres ### Syntax Examples: ### - make {command} TARGET={database} ENVIRONMENT={environment} @@ -20,7 +27,7 @@ DATABASE_URI = postgresql://$(USER):$(PASSWORD)@$(HOST):$(PORT)/$(TARGET)$(SUFIX PSQL = psql "$(DATABASE_URI)" -f migrate: - $(PSQL) migrations/V1__create_$(TARGET)_schema.sql + $(PSQL) db/$(TARGET)/migrations/V1__create_$(TARGET)_schema.sql schema: $(PSQL) db/$(TARGET)/schema.sql @@ -35,14 +42,18 @@ enums: $(PSQL) db/$(TARGET)/enums.sql reset: - $(PSQL) db/reset.sql -v DB=$(TARGET) + psql "$(MAINT_URI)" -f db/reset.sql -v DB=$(DB_NAME) $(PSQL) db/$(TARGET)/schema.sql $(PSQL) db/$(TARGET)/enums.sql $(PSQL) db/$(TARGET)/seed.sql $(PSQL) db/$(TARGET)/indexes.sql +### dataload nao usa TARGET: auth_user/users compartilham UUID entre +### coredb e authdb, entao sempre popula os dois bancos juntos (ver +### scripts/dataload.py -- AUTH_STEPS roda contra authdb, CORE_STEPS +### contra coredb, na mesma execucao). dataload: - python -m scripts.dataload $(TARGET) $(ROWS) + python -m scripts.dataload $(ROWS) backup: pg_dump "$(DATABASE_URI)" > db/$(TARGET)/backup.sql diff --git a/db/core/migrations/V1__create_core_schema.sql b/db/core/migrations/V1__create_core_schema.sql index 2055432..a6e6de8 100644 --- a/db/core/migrations/V1__create_core_schema.sql +++ b/db/core/migrations/V1__create_core_schema.sql @@ -145,7 +145,7 @@ CREATE TABLE contact ( CREATE TABLE position ( id UUID NOT NULL DEFAULT gen_random_uuid(), name VARCHAR(12) NOT NULL, - accesses TEXT NOT NULL, + accesses VARCHAR(255) NOT NULL, CONSTRAINT pk_position PRIMARY KEY (id) ); @@ -191,16 +191,6 @@ CREATE TABLE company_plans ( CONSTRAINT pk_company_plans PRIMARY KEY (id) ); -CREATE TABLE certification ( - id UUID NOT NULL DEFAULT gen_random_uuid(), - name VARCHAR(100), - issuer VARCHAR(100), - validity TIMESTAMP, - description TEXT, - - CONSTRAINT pk_certification PRIMARY KEY (id) -); - -- auth_id referencia auth_user.id no banco do api-auth (outro -- microsservico/banco) -- sem FK real aqui de proposito, so o valor solto. -- username = handle publico do usuario (ex.: "@joaosilva"), sempre minusculo @@ -225,8 +215,8 @@ CREATE TABLE users ( CREATE TABLE company ( id UUID NOT NULL DEFAULT gen_random_uuid(), status company_status NOT NULL DEFAULT 'UNDER_ANALYSIS', - fk_address UUID, - fk_business_contact UUID, + fk_address UUID NOT NULL, + fk_business_contact UUID NOT NULL, cnpj VARCHAR(14) NOT NULL, trade_name VARCHAR(120) NOT NULL, corporate_name VARCHAR(120) NOT NULL, @@ -287,6 +277,17 @@ CREATE TABLE requester ( REFERENCES company (id) ); +-- Marca uma empresa como prestadora de servico tecnico (alem de +-- fornecedora/solicitante). Sem seed proprio ainda -- ver acompanhamento. +CREATE TABLE technical_company ( + id UUID NOT NULL DEFAULT gen_random_uuid(), + fk_company UUID NOT NULL, + + CONSTRAINT pk_technical_company PRIMARY KEY (id), + CONSTRAINT fk_technical_company_company FOREIGN KEY (fk_company) + REFERENCES company (id) +); + CREATE TABLE technician ( id UUID NOT NULL DEFAULT gen_random_uuid(), fk_person UUID NOT NULL, @@ -297,6 +298,24 @@ CREATE TABLE technician ( REFERENCES person (id) ); +-- Certificacao tecnica de um tecnico especifico (ex.: NR-10, NR-35). +-- "type" e o rotulo curto/categoria; "name"/"issuer" sao complementares. +CREATE TABLE certification ( + id UUID NOT NULL DEFAULT gen_random_uuid(), + fk_technician UUID NOT NULL, + type VARCHAR(255) NOT NULL, + information TEXT NOT NULL, + image TEXT, + description TEXT, + issuer VARCHAR(100), + name VARCHAR(100), + validity TIMESTAMP, + + CONSTRAINT pk_certification PRIMARY KEY (id), + CONSTRAINT fk_certification_technician FOREIGN KEY (fk_technician) + REFERENCES technician (id) +); + -- Tabela associativa company <-> users, com o cargo do usuario na empresa. CREATE TABLE user_company ( id UUID NOT NULL DEFAULT gen_random_uuid(), @@ -359,7 +378,7 @@ CREATE TABLE certification_record ( CREATE TABLE technician_affiliation ( id UUID NOT NULL DEFAULT gen_random_uuid(), - fk_company UUID, + fk_company UUID NOT NULL, fk_technician UUID NOT NULL, affiliation_type technical_affiliation_type NOT NULL, active BOOLEAN NOT NULL DEFAULT true, @@ -467,7 +486,7 @@ CREATE TABLE geolocalization ( CREATE TABLE local_unit ( id UUID NOT NULL DEFAULT gen_random_uuid(), fk_requester UUID NOT NULL, - fk_address UUID, + fk_address UUID NOT NULL, complement VARCHAR(255), location_type location_type NOT NULL, @@ -481,8 +500,8 @@ CREATE TABLE local_unit ( CREATE TABLE unit_specifications ( id UUID NOT NULL DEFAULT gen_random_uuid(), fk_local_unit UUID NOT NULL, - specifications TEXT, - location_photos TEXT, + specifications VARCHAR(255), + location_photos VARCHAR(255), date TIMESTAMPTZ NOT NULL, CONSTRAINT pk_unit_specifications PRIMARY KEY (id), @@ -495,10 +514,10 @@ CREATE TABLE unit_specifications ( -- ----------------------------------------------------------------------------- CREATE TABLE technical_project ( id UUID NOT NULL DEFAULT gen_random_uuid(), - fk_requester UUID, - fk_local_unit UUID, - status service_status, - start_date TIMESTAMPTZ, + fk_requester UUID NOT NULL, + fk_local_unit UUID NOT NULL, + status service_status NOT NULL DEFAULT 'OPEN', + start_date TIMESTAMPTZ NOT NULL DEFAULT now(), end_date TIMESTAMP, CONSTRAINT pk_technical_project PRIMARY KEY (id), @@ -563,7 +582,7 @@ CREATE TABLE professional_review ( fk_reviewer UUID NOT NULL, fk_service UUID NOT NULL, rating NUMERIC(2, 1) NOT NULL, - comment TEXT, + comment VARCHAR(255), active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), @@ -609,7 +628,7 @@ CREATE TABLE proposal ( id UUID NOT NULL DEFAULT gen_random_uuid(), fk_requester UUID NOT NULL, status proposal_status NOT NULL DEFAULT 'AWAITING_SUPPLIER', - notes TEXT, + notes VARCHAR(255), total_amount NUMERIC, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ, diff --git a/db/core/schema.sql b/db/core/schema.sql index 9f0bdaa..fef19c7 100644 --- a/db/core/schema.sql +++ b/db/core/schema.sql @@ -61,7 +61,7 @@ CREATE TABLE contact ( CREATE TABLE position ( id UUID NOT NULL DEFAULT gen_random_uuid(), name VARCHAR(12) NOT NULL, - accesses TEXT NOT NULL, + accesses VARCHAR(255) NOT NULL, CONSTRAINT pk_position PRIMARY KEY (id) ); @@ -107,16 +107,6 @@ CREATE TABLE company_plans ( CONSTRAINT pk_company_plans PRIMARY KEY (id) ); -CREATE TABLE certification ( - id UUID NOT NULL DEFAULT gen_random_uuid(), - name VARCHAR(100), - issuer VARCHAR(100), - validity TIMESTAMP, - description TEXT, - - CONSTRAINT pk_certification PRIMARY KEY (id) -); - -- auth_id referencia auth_user.id no banco do api-auth (outro -- microsservico/banco) -- sem FK real aqui de proposito, so o valor solto. -- username = handle publico do usuario (ex.: "@joaosilva"), sempre minusculo @@ -141,8 +131,8 @@ CREATE TABLE users ( CREATE TABLE company ( id UUID NOT NULL DEFAULT gen_random_uuid(), status company_status NOT NULL DEFAULT 'UNDER_ANALYSIS', - fk_address UUID, - fk_business_contact UUID, + fk_address UUID NOT NULL, + fk_business_contact UUID NOT NULL, cnpj VARCHAR(14) NOT NULL, trade_name VARCHAR(120) NOT NULL, corporate_name VARCHAR(120) NOT NULL, @@ -203,6 +193,17 @@ CREATE TABLE requester ( REFERENCES company (id) ); +-- Marca uma empresa como prestadora de servico tecnico (alem de +-- fornecedora/solicitante). Sem seed proprio ainda -- ver acompanhamento. +CREATE TABLE technical_company ( + id UUID NOT NULL DEFAULT gen_random_uuid(), + fk_company UUID NOT NULL, + + CONSTRAINT pk_technical_company PRIMARY KEY (id), + CONSTRAINT fk_technical_company_company FOREIGN KEY (fk_company) + REFERENCES company (id) +); + CREATE TABLE technician ( id UUID NOT NULL DEFAULT gen_random_uuid(), fk_person UUID NOT NULL, @@ -213,6 +214,24 @@ CREATE TABLE technician ( REFERENCES person (id) ); +-- Certificacao tecnica de um tecnico especifico (ex.: NR-10, NR-35). +-- "type" e o rotulo curto/categoria; "name"/"issuer" sao complementares. +CREATE TABLE certification ( + id UUID NOT NULL DEFAULT gen_random_uuid(), + fk_technician UUID NOT NULL, + type VARCHAR(255) NOT NULL, + information TEXT NOT NULL, + image TEXT, + description TEXT, + issuer VARCHAR(100), + name VARCHAR(100), + validity TIMESTAMP, + + CONSTRAINT pk_certification PRIMARY KEY (id), + CONSTRAINT fk_certification_technician FOREIGN KEY (fk_technician) + REFERENCES technician (id) +); + -- Tabela associativa company <-> users, com o cargo do usuario na empresa. CREATE TABLE user_company ( id UUID NOT NULL DEFAULT gen_random_uuid(), @@ -275,7 +294,7 @@ CREATE TABLE certification_record ( CREATE TABLE technician_affiliation ( id UUID NOT NULL DEFAULT gen_random_uuid(), - fk_company UUID, + fk_company UUID NOT NULL, fk_technician UUID NOT NULL, affiliation_type technical_affiliation_type NOT NULL, active BOOLEAN NOT NULL DEFAULT true, @@ -383,7 +402,7 @@ CREATE TABLE geolocalization ( CREATE TABLE local_unit ( id UUID NOT NULL DEFAULT gen_random_uuid(), fk_requester UUID NOT NULL, - fk_address UUID, + fk_address UUID NOT NULL, complement VARCHAR(255), location_type location_type NOT NULL, @@ -397,8 +416,8 @@ CREATE TABLE local_unit ( CREATE TABLE unit_specifications ( id UUID NOT NULL DEFAULT gen_random_uuid(), fk_local_unit UUID NOT NULL, - specifications TEXT, - location_photos TEXT, + specifications VARCHAR(255), + location_photos VARCHAR(255), date TIMESTAMPTZ NOT NULL, CONSTRAINT pk_unit_specifications PRIMARY KEY (id), @@ -411,10 +430,10 @@ CREATE TABLE unit_specifications ( -- ----------------------------------------------------------------------------- CREATE TABLE technical_project ( id UUID NOT NULL DEFAULT gen_random_uuid(), - fk_requester UUID, - fk_local_unit UUID, - status service_status, - start_date TIMESTAMPTZ, + fk_requester UUID NOT NULL, + fk_local_unit UUID NOT NULL, + status service_status NOT NULL DEFAULT 'OPEN', + start_date TIMESTAMPTZ NOT NULL DEFAULT now(), end_date TIMESTAMP, CONSTRAINT pk_technical_project PRIMARY KEY (id), @@ -479,7 +498,7 @@ CREATE TABLE professional_review ( fk_reviewer UUID NOT NULL, fk_service UUID NOT NULL, rating NUMERIC(2, 1) NOT NULL, - comment TEXT, + comment VARCHAR(255), active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), @@ -525,7 +544,7 @@ CREATE TABLE proposal ( id UUID NOT NULL DEFAULT gen_random_uuid(), fk_requester UUID NOT NULL, status proposal_status NOT NULL DEFAULT 'AWAITING_SUPPLIER', - notes TEXT, + notes VARCHAR(255), total_amount NUMERIC, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ, diff --git a/db/core/seed.sql b/db/core/seed.sql index be94978..00a9551 100644 --- a/db/core/seed.sql +++ b/db/core/seed.sql @@ -62,10 +62,18 @@ VALUES ( ); -- Empresa solicitante, com uma unidade de instalacao. -INSERT INTO company (id, status, cnpj, trade_name, corporate_name) +INSERT INTO address (id, state, city, zip_code, street, number) +VALUES ('99999999-1111-1111-1111-111111111111', 'SP', 'Campinas', '13010000', 'Rua das Flores', '250'); + +INSERT INTO business_contact (id, company_email, phone) +VALUES ('88888888-1111-1111-1111-111111111111', 'contato@requester-demo.dev', '11999991111'); + +INSERT INTO company (id, status, fk_address, fk_business_contact, cnpj, trade_name, corporate_name) VALUES ( 'bbbbbbbb-1111-1111-1111-111111111111', 'APPROVED', + '99999999-1111-1111-1111-111111111111', + '88888888-1111-1111-1111-111111111111', '11111111000191', 'Requester Demo', 'Requester Demo Energia Ltda' diff --git a/scripts/connection.py b/scripts/connection.py index 98d403d..a67e967 100644 --- a/scripts/connection.py +++ b/scripts/connection.py @@ -3,26 +3,29 @@ import psycopg2 import psycopg2.extras from dotenv import load_dotenv -from scripts.databases import DATABASES psycopg2.extras.register_uuid() load_dotenv() -TARGETS = { - "core" : DATABASES.CORE, - "auth" : DATABASES.AUTH -} +TARGETS = ("core", "auth") -def connect(target: str) -> None: + +def connect(target: str): + """Conecta em coredb/authdb usando as MESMAS credenciais e a mesma + convencao de nome de banco do Makefile ($(TARGET)db$(SUFIX)): qa usa + sufixo "qa" (coredbqa/authdbqa, banco compartilhado com prod no mesmo + host Aiven); local e prod usam o nome puro (coredb/authdb), cada um no + seu proprio host -- confirmado contra os secrets reais do Infisical. + """ if target not in TARGETS: raise ValueError(f"Target not found: {target!r}. Use one of them: {list(TARGETS)}") - prefix: str = TARGETS[target].value + suffix = "qa" if os.environ.get("ENVIRONMENT", "").lower() == "qa" else "" return psycopg2.connect( - host=os.environ[f"{prefix}_HOST"], - port=os.environ[f"{prefix}_PORT"], - dbname=os.environ[f"{prefix}_NAME"], - user=os.environ[f"{prefix}_USER"], - password=os.environ[f"{prefix}_PASS"], - sslmode=os.environ.get(f"{prefix}_SSLMODE", "require"), + host=os.environ["HOST"], + port=os.environ["PORT"], + dbname=f"{target}db{suffix}", + user=os.environ["USER"], + password=os.environ["PASSWORD"], + sslmode=os.environ.get("SSLMODE", "require"), ) diff --git a/scripts/data/catalog/business_note.csv b/scripts/data/catalog/business_note.csv new file mode 100644 index 0000000..262b8c5 --- /dev/null +++ b/scripts/data/catalog/business_note.csv @@ -0,0 +1,16 @@ +note +Cliente solicitou revisao de escopo antes da aprovacao final. +Aguardando confirmacao de disponibilidade de estoque do fornecedor. +Proposta inclui instalacao, homologacao e garantia de 12 meses. +Unidade com acesso restrito, agendar visita com antecedencia. +Cliente prioriza prazo de entrega sobre valor total. +Local com sombreamento parcial no periodo da tarde. +Telhado em boas condicoes, sem necessidade de reforco estrutural. +Proprietario solicitou orcamento com e sem financiamento. +Necessario laudo estrutural antes da instalacao. +Cliente ja possui sistema de monitoramento compativel. +Ponto de conexao a rede localizado no quadro de entrada. +Requer autorizacao de condominio antes da execucao. +Cliente indicado por outro cliente atendido anteriormente. +Visita tecnica confirmada para avaliacao inicial. +Projeto sujeito a ajuste apos vistoria da concessionaria. diff --git a/scripts/data/catalog/certification_description.csv b/scripts/data/catalog/certification_description.csv new file mode 100644 index 0000000..9fac7d7 --- /dev/null +++ b/scripts/data/catalog/certification_description.csv @@ -0,0 +1,9 @@ +description +Certificacao emitida apos avaliacao teorica e pratica do candidato. +Valida a competencia tecnica do profissional para o servico especificado. +Reconhecida pelo orgao de classe e exigida em contratos de maior porte. +Requer atualizacao periodica para manter a validade. +Inclui modulo de seguranca do trabalho e boas praticas de instalacao. +Emitida por instituicao credenciada, com registro nacional. +Cobre normas tecnicas vigentes e procedimentos de campo. +Avalia tanto conhecimento normativo quanto execucao pratica supervisionada. diff --git a/scripts/data/catalog/certification_name.csv b/scripts/data/catalog/certification_name.csv new file mode 100644 index 0000000..3051fc4 --- /dev/null +++ b/scripts/data/catalog/certification_name.csv @@ -0,0 +1,16 @@ +name +NR-10 Seguranca em Instalacoes e Servicos em Eletricidade +NR-35 Trabalho em Altura +Instalador Certificado PV Solar Edge +Instalador Certificado PV Growatt +Certificacao Inversor Fronius +Certificacao Inversor Huawei FusionSolar +Tecnico em Sistemas Fotovoltaicos - ABGD +Auditor de Qualidade em Energia Solar +NBR 16690 Instalacoes Eletricas de Sistemas Fotovoltaicos +NBR 5410 Instalacoes Eletricas de Baixa Tensao +Certificacao NABCEP Installation Professional +Curso Avancado de Dimensionamento Fotovoltaico +Certificacao em Protecao contra Descargas Atmosfericas +Tecnico em Manutencao de Baterias Estacionarias +Certificacao em Conexao a Rede de Distribuicao (GD) diff --git a/scripts/data/catalog/company_plans.csv b/scripts/data/catalog/company_plans.csv new file mode 100644 index 0000000..30561f6 --- /dev/null +++ b/scripts/data/catalog/company_plans.csv @@ -0,0 +1,5 @@ +name,value,cycle +Basic,99.90,MONTHLY +Pro,249.90,MONTHLY +Pro Anual,2399.90,YEARLY +Enterprise,699.90,QUARTERLY diff --git a/scripts/data/catalog/permission.csv b/scripts/data/catalog/permission.csv new file mode 100644 index 0000000..78e25a1 --- /dev/null +++ b/scripts/data/catalog/permission.csv @@ -0,0 +1,11 @@ +permission_name,description +company:read,Ver empresas +company:write,Editar empresas +proposal:read,Ver propostas +proposal:write,Editar propostas +catalog:read,Ver catalogo +catalog:write,Editar catalogo +service:read,Ver servicos +service:write,Editar servicos +billing:read,Ver cobrancas +billing:write,Editar cobrancas diff --git a/scripts/data/catalog/position.csv b/scripts/data/catalog/position.csv new file mode 100644 index 0000000..6f2740e --- /dev/null +++ b/scripts/data/catalog/position.csv @@ -0,0 +1,6 @@ +name,accesses +ADMIN,full access +MANAGER,company management +TECHNICIAN,field service +SALES,proposals and offers +SUPPORT,customer support diff --git a/scripts/data/catalog/profession.csv b/scripts/data/catalog/profession.csv new file mode 100644 index 0000000..2e81002 --- /dev/null +++ b/scripts/data/catalog/profession.csv @@ -0,0 +1,9 @@ +name +Eletricista +Engenheiro Eletricista +Tecnico em Eletronica +Instalador Solar +Projetista +Gestor de Obras +Soldador +Encanador diff --git a/scripts/data/catalog/review_comment.csv b/scripts/data/catalog/review_comment.csv new file mode 100644 index 0000000..84ef239 --- /dev/null +++ b/scripts/data/catalog/review_comment.csv @@ -0,0 +1,16 @@ +comment +Atendimento pontual e instalacao muito bem executada. +Tecnico explicou todo o processo com clareza, recomendo. +Servico rapido, mas faltou orientacao sobre manutencao. +Equipe cuidadosa, deixou o local limpo apos o servico. +Profissional experiente, resolveu o problema na primeira visita. +Comunicacao poderia ser melhor durante o agendamento. +Excelente trabalho, sistema funcionando perfeitamente desde entao. +Atraso no horario combinado, mas servico de qualidade. +Muito atencioso e respondeu todas as duvidas. +Instalacao dentro do prazo previsto, sem imprevistos. +Faltou revisar a fiacao antes de finalizar o servico. +Superou as expectativas, equipamento bem configurado. +Bom custo-beneficio pelo servico prestado. +Precisou de uma segunda visita para ajuste fino. +Recomendo fortemente, profissionalismo do inicio ao fim. diff --git a/scripts/data/catalog/security_event_note.csv b/scripts/data/catalog/security_event_note.csv new file mode 100644 index 0000000..47a28d1 --- /dev/null +++ b/scripts/data/catalog/security_event_note.csv @@ -0,0 +1,7 @@ +note +Acesso realizado a partir de dispositivo ja conhecido. +Tentativa de acesso a partir de novo dispositivo. +Endereco IP fora do padrao usual do usuario. +Sessao encerrada por inatividade. +Verificacao adicional solicitada por politica de seguranca. +Evento registrado automaticamente pelo sistema de autenticacao. diff --git a/scripts/data/catalog/technical_service_purpose.csv b/scripts/data/catalog/technical_service_purpose.csv new file mode 100644 index 0000000..963d091 --- /dev/null +++ b/scripts/data/catalog/technical_service_purpose.csv @@ -0,0 +1,16 @@ +purpose +Instalacao de sistema fotovoltaico residencial +Instalacao de sistema fotovoltaico comercial +Manutencao preventiva de inversor +Manutencao corretiva de string box +Limpeza de modulos fotovoltaicos +Substituicao de modulo danificado +Vistoria tecnica para homologacao junto a distribuidora +Ampliacao de usina fotovoltaica existente +Troca de bateria estacionaria +Diagnostico de queda de geracao +Instalacao de estrutura de fixacao em telhado ceramico +Instalacao de estrutura de fixacao em telhado metalico +Configuracao remota de monitoramento de geracao +Reparo em aterramento e protecao contra surtos +Revisao anual de garantia do sistema diff --git a/scripts/data/cloudinary/banner.csv b/scripts/data/cloudinary/banner.csv new file mode 100644 index 0000000..97aa871 --- /dev/null +++ b/scripts/data/cloudinary/banner.csv @@ -0,0 +1,51 @@ +url,public_id +https://res.cloudinary.com/solaria/image/upload/v1789135662/banner-urban-01.jpg,banner-urban-01 +https://res.cloudinary.com/solaria/image/upload/v1789135665/banner-urban-02.jpg,banner-urban-02 +https://res.cloudinary.com/solaria/image/upload/v1789135669/banner-urban-03.jpg,banner-urban-03 +https://res.cloudinary.com/solaria/image/upload/v1789135672/banner-urban-04.jpg,banner-urban-04 +https://res.cloudinary.com/solaria/image/upload/v1789135675/banner-urban-05.jpg,banner-urban-05 +https://res.cloudinary.com/solaria/image/upload/v1789135678/banner-urban-06.jpg,banner-urban-06 +https://res.cloudinary.com/solaria/image/upload/v1789135680/banner-urban-07.jpg,banner-urban-07 +https://res.cloudinary.com/solaria/image/upload/v1789135683/banner-urban-08.jpg,banner-urban-08 +https://res.cloudinary.com/solaria/image/upload/v1789135686/banner-urban-09.jpg,banner-urban-09 +https://res.cloudinary.com/solaria/image/upload/v1789135689/banner-urban-10.jpg,banner-urban-10 +https://res.cloudinary.com/solaria/image/upload/v1789135696/banner-nature-01.jpg,banner-nature-01 +https://res.cloudinary.com/solaria/image/upload/v1789135699/banner-nature-02.jpg,banner-nature-02 +https://res.cloudinary.com/solaria/image/upload/v1789135701/banner-nature-03.jpg,banner-nature-03 +https://res.cloudinary.com/solaria/image/upload/v1789135704/banner-nature-04.jpg,banner-nature-04 +https://res.cloudinary.com/solaria/image/upload/v1789135708/banner-nature-05.jpg,banner-nature-05 +https://res.cloudinary.com/solaria/image/upload/v1789135711/banner-nature-06.jpg,banner-nature-06 +https://res.cloudinary.com/solaria/image/upload/v1789135714/banner-nature-07.jpg,banner-nature-07 +https://res.cloudinary.com/solaria/image/upload/v1789135716/banner-nature-08.jpg,banner-nature-08 +https://res.cloudinary.com/solaria/image/upload/v1789135720/banner-nature-09.jpg,banner-nature-09 +https://res.cloudinary.com/solaria/image/upload/v1789135723/banner-nature-10.jpg,banner-nature-10 +https://res.cloudinary.com/solaria/image/upload/v1789135729/banner-space-01.jpg,banner-space-01 +https://res.cloudinary.com/solaria/image/upload/v1789135732/banner-space-02.jpg,banner-space-02 +https://res.cloudinary.com/solaria/image/upload/v1789135735/banner-space-03.jpg,banner-space-03 +https://res.cloudinary.com/solaria/image/upload/v1789135738/banner-space-04.jpg,banner-space-04 +https://res.cloudinary.com/solaria/image/upload/v1789135741/banner-space-05.jpg,banner-space-05 +https://res.cloudinary.com/solaria/image/upload/v1789135744/banner-space-06.jpg,banner-space-06 +https://res.cloudinary.com/solaria/image/upload/v1789135747/banner-space-07.jpg,banner-space-07 +https://res.cloudinary.com/solaria/image/upload/v1789135749/banner-space-08.jpg,banner-space-08 +https://res.cloudinary.com/solaria/image/upload/v1789135752/banner-space-09.jpg,banner-space-09 +https://res.cloudinary.com/solaria/image/upload/v1789135755/banner-space-10.jpg,banner-space-10 +https://res.cloudinary.com/solaria/image/upload/v1789135763/banner-animals-01.jpg,banner-animals-01 +https://res.cloudinary.com/solaria/image/upload/v1789135766/banner-animals-02.jpg,banner-animals-02 +https://res.cloudinary.com/solaria/image/upload/v1789135768/banner-animals-03.jpg,banner-animals-03 +https://res.cloudinary.com/solaria/image/upload/v1789135771/banner-animals-04.jpg,banner-animals-04 +https://res.cloudinary.com/solaria/image/upload/v1789135774/banner-animals-05.jpg,banner-animals-05 +https://res.cloudinary.com/solaria/image/upload/v1789135777/banner-animals-06.jpg,banner-animals-06 +https://res.cloudinary.com/solaria/image/upload/v1789135779/banner-animals-07.jpg,banner-animals-07 +https://res.cloudinary.com/solaria/image/upload/v1789135782/banner-animals-08.jpg,banner-animals-08 +https://res.cloudinary.com/solaria/image/upload/v1789135786/banner-animals-09.jpg,banner-animals-09 +https://res.cloudinary.com/solaria/image/upload/v1789135789/banner-animals-10.jpg,banner-animals-10 +https://res.cloudinary.com/solaria/image/upload/v1789135795/banner-ocean-01.jpg,banner-ocean-01 +https://res.cloudinary.com/solaria/image/upload/v1789135798/banner-ocean-02.jpg,banner-ocean-02 +https://res.cloudinary.com/solaria/image/upload/v1789135801/banner-ocean-03.jpg,banner-ocean-03 +https://res.cloudinary.com/solaria/image/upload/v1789135804/banner-ocean-04.jpg,banner-ocean-04 +https://res.cloudinary.com/solaria/image/upload/v1789135807/banner-ocean-05.jpg,banner-ocean-05 +https://res.cloudinary.com/solaria/image/upload/v1789135810/banner-ocean-06.jpg,banner-ocean-06 +https://res.cloudinary.com/solaria/image/upload/v1789135813/banner-ocean-07.jpg,banner-ocean-07 +https://res.cloudinary.com/solaria/image/upload/v1789135816/banner-ocean-08.jpg,banner-ocean-08 +https://res.cloudinary.com/solaria/image/upload/v1789135818/banner-ocean-09.jpg,banner-ocean-09 +https://res.cloudinary.com/solaria/image/upload/v1789135822/banner-ocean-10.jpg,banner-ocean-10 diff --git a/scripts/data/cloudinary/hero.csv b/scripts/data/cloudinary/hero.csv deleted file mode 100644 index 7eede8a..0000000 --- a/scripts/data/cloudinary/hero.csv +++ /dev/null @@ -1,11 +0,0 @@ -url,public_id -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-instalacao-residencial.jpg,solier/banners/catalog/banner-instalacao-residencial -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-instalacao-industrial.jpg,solier/banners/catalog/banner-instalacao-industrial -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-usina-solar-aerea.jpg,solier/banners/catalog/banner-usina-solar-aerea -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-equipe-tecnica-campo.jpg,solier/banners/catalog/banner-equipe-tecnica-campo -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-galpao-distribuicao.jpg,solier/banners/catalog/banner-galpao-distribuicao -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-escritorio-empresa.jpg,solier/banners/catalog/banner-escritorio-empresa -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-por-do-sol-paineis.jpg,solier/banners/catalog/banner-por-do-sol-paineis -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-fabrica-montagem.jpg,solier/banners/catalog/banner-fabrica-montagem -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-manutencao-tecnico.jpg,solier/banners/catalog/banner-manutencao-tecnico -https://res.cloudinary.com/solier/image/upload/v1700000000/solier/banners/catalog/banner-certificacao-qualidade.jpg,solier/banners/catalog/banner-certificacao-qualidade diff --git a/scripts/data/cloudinary/unit_specifications_photos.csv b/scripts/data/cloudinary/unit_specifications_photos.csv new file mode 100644 index 0000000..79384be --- /dev/null +++ b/scripts/data/cloudinary/unit_specifications_photos.csv @@ -0,0 +1 @@ +url,public_id diff --git a/scripts/databases.py b/scripts/databases.py deleted file mode 100644 index 69248d9..0000000 --- a/scripts/databases.py +++ /dev/null @@ -1,5 +0,0 @@ -from enum import Enum - -class DATABASES(Enum): - CORE = "coredb" - AUTH = "authdb" diff --git a/scripts/dataload.py b/scripts/dataload.py index d977e16..bf6217d 100644 --- a/scripts/dataload.py +++ b/scripts/dataload.py @@ -12,11 +12,16 @@ random.seed(42) CLOUDINARY_DIR = Path(__file__).resolve().parent / "data" / "cloudinary" +CATALOG_DIR = Path(__file__).resolve().parent / "data" / "catalog" _media_pools: dict[str, list[tuple[str, str]]] = {} +_catalogs: dict[str, list[dict]] = {} def trunc(value: str, length: int) -> str: return value[:length] def digits(n: int) -> str: return "".join(random.choices(string.digits, k=n)) +def digits_only(value: str) -> str: return re.sub(r"\D", "", value) +def gen_cpf() -> str: return digits_only(FAKE.cpf()) +def gen_cnpj() -> str: return digits_only(FAKE.cnpj()) def new_id() -> uuid.UUID: return uuid.uuid4() def pick(seq): return random.choice(seq) def maybe(seq, p=0.7): return pick(seq) if random.random() < p else None @@ -31,23 +36,51 @@ def unique_username() -> str: def media_pool(name: str) -> list[tuple[str, str]]: - """Le (e cacheia) as linhas fixas de scripts/data/cloudinary/.csv como (url, public_id).""" + """Le (e cacheia) as linhas de scripts/data/cloudinary/.csv como (url, public_id). + Pode estar vazio -- cada arquivo cobre um unico campo de imagem e e + curado manualmente; use pick_media() para ter fallback automatico + enquanto o CSV nao foi preenchido.""" if name not in _media_pools: path = CLOUDINARY_DIR / f"{name}.csv" with open(path, newline="", encoding="utf-8") as fh: - pool = [(row["url"], row["public_id"]) for row in csv.DictReader(fh)] - if not pool: - raise ValueError(f"CSV sem linhas: {path}") - _media_pools[name] = pool + _media_pools[name] = [(row["url"], row["public_id"]) for row in csv.DictReader(fh)] return _media_pools[name] +def pick_media(name: str) -> tuple[str, str]: + """Escolhe (url, public_id) do pool .csv; se o CSV ainda estiver + vazio, gera um placeholder via Faker para o dataload continuar rodando.""" + pool = media_pool(name) + return pick(pool) if pool else (FAKE.image_url(), uuid.uuid4().hex) + + +def catalog(name: str) -> list[dict]: + """Le (e cacheia) scripts/data/catalog/.csv como lista de dicts -- + vocabulario fixo (cargos, permissoes, planos etc.) editavel sem tocar + no codigo.""" + if name not in _catalogs: + path = CATALOG_DIR / f"{name}.csv" + with open(path, newline="", encoding="utf-8") as fh: + _catalogs[name] = list(csv.DictReader(fh)) + return _catalogs[name] + + +def pick_text(catalog_name: str, column: str) -> str: + """Escolhe um valor em portugues de scripts/data/catalog/.csv. + Usado no lugar de FAKE.text()/FAKE.sentence()/FAKE.bs(), que geram + lorem ipsum pseudo-latino ou ingles mesmo com o locale pt_BR.""" + return pick(catalog(catalog_name))[column] + + class Seeder: - def __init__(self, conn, scale: float): - self.conn = conn + def __init__(self, scale: float): + self.conn = None self.scale = scale self.ids = {} + def use_connection(self, conn) -> None: + self.conn = conn + def n(self, base: int) -> int: return max(1, round(base * self.scale)) @@ -96,17 +129,6 @@ def seed_geolocalization(self): )) self.insert("geolocalization", ["id", "fk_address", "latitude", "longitude"], rows) - def seed_media_assets(self, count: int, pool: str) -> list[uuid.UUID]: - rows = [] - ids = [] - for _ in range(count): - row_id = new_id() - ids.append(row_id) - url, public_id = pick(media_pool(pool)) - rows.append((row_id, url, public_id, FAKE.date_time_between("-2y", "now"))) - self.insert("media_asset", ["id", "url", "public_id", "created_at"], rows) - return ids - def seed_auth_user(self): rows = [] for _ in range(self.n(300)): @@ -145,11 +167,12 @@ def seed_local_credential(self): def seed_federated_identity(self): rows = [] - for _ in range(self.n(50)): + sample_size = min(len(self.ids["auth_user"]), self.n(50)) + for user_id in random.sample(self.ids["auth_user"], k=sample_size): row_id = new_id() rows.append(( row_id, - pick(self.ids["auth_user"]), + user_id, "FIREBASE", "https://securetoken.google.com/solaria", uuid.uuid4().hex, @@ -159,7 +182,7 @@ def seed_federated_identity(self): maybe([FAKE.date_time_between("-30d", "now")]), )) self.insert("federated_identity", [ - "id", "fk_user", "authority", "issuer", "subject", "email", "email_verified", + "id", "user_id", "authority", "issuer", "subject", "email", "email_verified", "created_at", "last_login_at", ], rows) @@ -178,7 +201,7 @@ def seed_one_time_token(self): created, )) self.insert("one_time_token", [ - "id", "fk_user", "token_hash", "type", "expires_at", "consumed_at", "created_at", + "id", "user_id", "token_hash", "type", "expires_at", "consumed_at", "created_at", ], rows) def seed_auth_session(self): @@ -187,12 +210,14 @@ def seed_auth_session(self): row_id = new_id() created = FAKE.date_time_between("-180d", "now") revoked = random.random() < 0.2 + methods = random.sample(["PASSWORD", "TOTP", "FEDERATED_FIREBASE"], k=random.randint(1, 2)) rows.append(( row_id, pick(self.ids["auth_user"]), FAKE.ipv4(), trunc(FAKE.user_agent(), 500), pick(["web", "android", "ios"]), + methods, maybe([created], p=0.3), created, created + timedelta(days=random.randint(0, 30)), @@ -202,17 +227,10 @@ def seed_auth_session(self): )) self.ids.setdefault("auth_session", []).extend(r[0] for r in rows) self.insert("auth_session", [ - "id", "fk_user", "ip_address", "user_agent", "device", "mfa_completed_at", - "created_at", "last_access_at", "expires_at", "revoked_at", "revocation_reason", + "id", "user_id", "ip_address", "user_agent", "device", "authentication_methods", + "mfa_completed_at", "created_at", "last_access_at", "expires_at", "revoked_at", "revocation_reason", ], rows) - def seed_session_authentication_method(self): - rows = [] - for session_id in self.ids["auth_session"]: - for method in random.sample(["PASSWORD", "TOTP", "FEDERATED_FIREBASE"], k=random.randint(1, 2)): - rows.append((session_id, method)) - self.insert("session_authentication_method", ["fk_session", "method"], rows) - def seed_refresh_token(self): chain_ids = [] rows = [] @@ -230,14 +248,14 @@ def seed_refresh_token(self): chain_ids.append((previous_id, row_id)) previous_id = row_id self.insert("refresh_token", [ - "id", "fk_session", "token_hash", "consumed_at", "revoked_at", "fk_replaced_by", + "id", "session_id", "token_hash", "consumed_at", "revoked_at", "replaced_by_id", "expires_at", "created_at", ], rows) if chain_ids: with self.conn.cursor() as cur: execute_values( cur, - "UPDATE refresh_token AS rt SET fk_replaced_by = data.next_id " + "UPDATE refresh_token AS rt SET replaced_by_id = data.next_id " "FROM (VALUES %s) AS data (prev_id, next_id) WHERE rt.id = data.prev_id", chain_ids, ) @@ -254,7 +272,7 @@ def seed_totp_factor(self): FAKE.date_time_between("-1y", "-6M"), FAKE.date_time_between("-6M", "now"), )) self.insert("totp_factor", [ - "id", "fk_user", "secret_ciphertext", "secret_nonce", "encryption_key_id", "algorithm", + "id", "user_id", "secret_ciphertext", "secret_nonce", "encryption_key_id", "algorithm", "digits", "period_seconds", "enabled_at", "last_used_counter", "created_at", "updated_at", ], rows) @@ -274,11 +292,11 @@ def seed_security_event(self): random.random() < 0.85, FAKE.ipv4(), trunc(FAKE.user_agent(), 500), - json.dumps({"note": FAKE.sentence()}), + json.dumps({"note": pick_text("security_event_note", "note")}), FAKE.date_time_between("-180d", "now"), )) self.insert("security_event", [ - "id", "fk_user", "fk_session", "event_type", "succeeded", "ip_address", "user_agent", + "id", "user_id", "session_id", "event_type", "succeeded", "ip_address", "user_agent", "details", "occurred_at", ], rows) @@ -305,8 +323,8 @@ def seed_users(self): row_id = new_id() rows.append(( row_id, auth_id, unique_username(), - maybe([pick(media_pool("profile"))[0]], p=0.5), - maybe([pick(media_pool("hero"))[0]], p=0.3), + maybe([pick_media("profile")[0]], p=0.5), + maybe([pick_media("banner")[0]], p=0.3), random.random() < 0.95, )) self.ids.setdefault("users", []).extend(r[0] for r in rows) @@ -318,28 +336,21 @@ def seed_person(self): row_id = new_id() rows.append(( row_id, user_id, maybe(self.ids["contact"]), trunc(FAKE.name(), 60), - digits(11), FAKE.date_of_birth(minimum_age=18, maximum_age=75), + gen_cpf(), FAKE.date_of_birth(minimum_age=18, maximum_age=75), )) self.ids.setdefault("person", []).extend(r[0] for r in rows) - self.insert("person", ["id", "fk_user", "fk_contact", "name", "cpf", "birth_date"], rows) + self.insert("person", ["id", "fk_users", "fk_contact", "name", "cpf", "birth_date"], rows) def seed_position(self): - names = [("ADMIN", "full access"), ("MANAGER", "company management"), - ("TECHNICIAN", "field service"), ("SALES", "proposals and offers"), - ("SUPPORT", "customer support")] - rows = [(new_id(), n, a) for n, a in names] + rows = [(new_id(), r["name"], r["accesses"]) for r in catalog("position")] self.ids["position"] = [r[0] for r in rows] self.insert("position", ["id", "name", "accesses"], rows) def seed_permission(self): - perms = [ - ("company:read", "Ver empresas"), ("company:write", "Editar empresas"), - ("proposal:read", "Ver propostas"), ("proposal:write", "Editar propostas"), - ("catalog:read", "Ver catalogo"), ("catalog:write", "Editar catalogo"), - ("service:read", "Ver servicos"), ("service:write", "Editar servicos"), - ("billing:read", "Ver cobrancas"), ("billing:write", "Editar cobrancas"), + rows = [ + (new_id(), r["permission_name"], r["permission_name"].split(":")[0].title(), r["description"]) + for r in catalog("permission") ] - rows = [(new_id(), code, code.split(":")[0].title(), desc) for code, desc in perms] self.ids["permission"] = [r[0] for r in rows] self.insert("permission", ["id", "permission_name", "name", "description"], rows) @@ -353,7 +364,7 @@ def seed_position_permission(self): continue seen.add(key) rows.append((new_id(), position_id, permission_id)) - self.insert("position_permission", ["id", "fk_position", "fk_permission"], rows) + self.insert("position_permission", ["id", "id_position", "id_permission"], rows) def seed_business_contact(self): rows = [] @@ -370,28 +381,17 @@ def seed_company(self): trade_name = trunc(FAKE.company(), 100) rows.append(( row_id, pick(["UNDER_ANALYSIS", "APPROVED", "APPROVED", "REJECTED"]), - maybe(self.ids["address"]), maybe(self.ids["business_contact"]), - digits(14), trade_name, trunc(FAKE.company() + " " + FAKE.company_suffix(), 120), - pick(["INSTALLER", "DISTRIBUTOR", "MANUFACTURER", "RESELLER"]), - trunc(f"{trade_name}-{uuid.uuid4().hex[:8]}".lower().replace(' ', '-'), 160), + pick(self.ids["address"]), pick(self.ids["business_contact"]), + gen_cnpj(), trade_name, trunc(FAKE.company() + " " + FAKE.company_suffix(), 120), )) self.ids.setdefault("company", []).extend(r[0] for r in rows) self.insert("company", [ "id", "status", "fk_address", "fk_business_contact", "cnpj", "trade_name", - "corporate_name", "business_type", "slug", + "corporate_name", ], rows) - def seed_company_photo(self): - profile_ids = self.seed_media_assets(self.n(20), "profile") - banner_ids = self.seed_media_assets(self.n(20), "hero") - rows = [(mid, pick(self.ids["company"]), "PROFILE") for mid in profile_ids] - rows += [(mid, pick(self.ids["company"]), "BANNER") for mid in banner_ids] - self.insert("company_photo", ["id", "fk_company", "type"], rows) - def seed_company_plans(self): - plans = [("Basic", 99.90, "MONTHLY"), ("Pro", 249.90, "MONTHLY"), - ("Pro Anual", 2399.90, "YEARLY"), ("Enterprise", 699.90, "QUARTERLY")] - rows = [(new_id(), n, v, c) for n, v, c in plans] + rows = [(new_id(), r["name"], float(r["value"]), r["cycle"]) for r in catalog("company_plans")] self.ids["company_plans"] = [r[0] for r in rows] self.insert("company_plans", ["id", "name", "value", "cycle"], rows) @@ -417,7 +417,7 @@ def seed_user_company(self): continue seen.add(key) rows.append((new_id(), company_id, user_id, pick(self.ids["position"]))) - self.insert("user_company", ["id", "fk_company", "fk_user", "fk_position"], rows) + self.insert("user_company", ["id", "fk_company", "fk_users", "fk_position"], rows) def seed_supplier(self): rows = [] @@ -464,91 +464,74 @@ def seed_model(self): row_id = new_id() rows.append(( row_id, trunc(FAKE.company(), 100), trunc(FAKE.bothify("Model-###??"), 100), - pick(["MONOCRYSTALLINE", "POLYCRYSTALLINE", "THIN_FILM"]), FAKE.pydecimal(left_digits=3, right_digits=2, positive=True), FAKE.pydecimal(left_digits=2, right_digits=2, positive=True), FAKE.pydecimal(left_digits=1, right_digits=2, positive=True), - FAKE.pydecimal(left_digits=1, right_digits=2, positive=True), FAKE.pydecimal(left_digits=2, right_digits=2, positive=True), pick(["APPROVED", "APPROVED", "UNDER_ANALYSIS", "REJECTED"]), )) self.ids.setdefault("model", []).extend(r[0] for r in rows) self.insert("model", [ - "id", "brand", "model", "type", "power_wp", "efficiency", "width", "length", "weight", "status", + "id", "brand", "model", "power_wp", "efficiency", "dimension", "weight", "status", ], rows) - def seed_model_photo(self): - ids = self.seed_media_assets(self.n(40), "panels") - rows = [(mid, pick(self.ids["model"])) for mid in ids] - self.insert("model_photo", ["id", "fk_model"], rows) - def seed_offer(self): rows = [] for _ in range(self.n(150)): row_id = new_id() - slug = trunc(f"offer-{uuid.uuid4().hex[:12]}", 160) rows.append(( row_id, pick(self.ids["supplier"]), pick(self.ids["model"]), FAKE.pydecimal(left_digits=4, right_digits=2, positive=True), random.randint(0, 500), - maybe([FAKE.date_time_between("now", "+1y")], p=0.5), slug, - maybe([FAKE.pydecimal(left_digits=2, right_digits=2, positive=True)], p=0.4), - maybe(["pt-BR", "en-US"], p=0.3), pick(["PENDING", "COMPLETED", "FAILED"]), + maybe([FAKE.date_time_between("now", "+1y")], p=0.5), )) self.ids.setdefault("offer", []).extend(r[0] for r in rows) self.insert("offer", [ - "id", "fk_supplier", "fk_model", "unit_price", "availability", "expiration_date", "slug", - "discount_percentage", "source_locale", "translation_status", + "id", "fk_supplier", "fk_model", "unit_price", "availability", "expiration_date", ], rows) - def seed_offer_service_region(self): - rows = [] - for offer_id in self.ids["offer"]: - for _ in range(random.randint(1, 3)): - rows.append((offer_id, trunc(FAKE.estado_sigla() + "-" + FAKE.city(), 120))) - self.insert("offer_service_region", ["fk_offer", "region"], list(set(rows))) - - def seed_offer_translation(self): - rows = [] - for offer_id in self.ids["offer"]: - for locale in random.sample(["pt-BR", "en-US", "es-ES"], k=random.randint(1, 2)): - rows.append(( - new_id(), offer_id, locale, trunc(FAKE.catch_phrase(), 160), - FAKE.text(200), maybe([FAKE.text(100)], p=0.4), - )) - self.insert("offer_translation", ["id", "fk_offer", "locale", "title", "description", "details"], rows) - def seed_inventory(self): + # UNIQUE(fk_supplier, fk_model) no banco real -- nao pode repetir o par. + max_pairs = len(self.ids["supplier"]) * len(self.ids["model"]) + target = min(self.n(150), max_pairs) rows = [] - for _ in range(self.n(150)): - rows.append((new_id(), pick(self.ids["supplier"]), pick(self.ids["model"]), random.randint(0, 1000))) + seen = set() + while len(rows) < target: + pair = (pick(self.ids["supplier"]), pick(self.ids["model"])) + if pair in seen: + continue + seen.add(pair) + rows.append((new_id(), pair[0], pair[1], random.randint(0, 1000))) self.insert("inventory", ["id", "fk_supplier", "fk_model", "quantity"], rows) def seed_profession(self): - names = ["Eletricista", "Engenheiro Eletricista", "Tecnico em Eletronica", - "Instalador Solar", "Projetista", "Gestor de Obras", "Soldador", "Encanador"] - rows = [(new_id(), n, random.random() < 0.3, random.random() < 0.6) for n in names] + rows = [ + (new_id(), r["name"], random.random() < 0.3, random.random() < 0.6) + for r in catalog("profession") + ] self.ids["profession"] = [r[0] for r in rows] self.insert("profession", ["id", "name", "accept_emergency_call", "requires_registration"], rows) + def seed_technician(self): + rows = [] + for person_id in random.sample(self.ids["person"], k=min(len(self.ids["person"]), self.n(100))): + row_id = new_id() + rows.append((row_id, person_id, trunc("CREA-" + FAKE.estado_sigla() + " " + digits(6), 60))) + self.ids.setdefault("technician", []).extend(r[0] for r in rows) + self.insert("technician", ["id", "fk_person", "crea"], rows) + def seed_certification(self): rows = [] for _ in range(self.n(20)): row_id = new_id() rows.append(( - row_id, trunc(FAKE.bs().title(), 100), trunc(FAKE.company(), 100), - FAKE.date_time_between("now", "+3y"), FAKE.text(150), + row_id, pick(self.ids["technician"]), + trunc(pick_text("certification_name", "name"), 255), + pick_text("certification_description", "description"), + trunc(FAKE.company(), 100), + maybe([FAKE.date_time_between("now", "+3y")], p=0.7), )) self.ids.setdefault("certification", []).extend(r[0] for r in rows) - self.insert("certification", ["id", "name", "issuer", "validity", "description"], rows) - - def seed_technician(self): - rows = [] - for person_id in random.sample(self.ids["person"], k=min(len(self.ids["person"]), self.n(100))): - row_id = new_id() - slug = trunc(f"tech-{uuid.uuid4().hex[:12]}", 160) - rows.append((row_id, person_id, trunc("CREA-" + FAKE.estado_sigla() + " " + digits(6), 60), slug)) - self.ids.setdefault("technician", []).extend(r[0] for r in rows) - self.insert("technician", ["id", "fk_person", "crea", "slug"], rows) + self.insert("certification", ["id", "fk_technician", "type", "information", "issuer", "validity"], rows) def seed_professional_registration(self): rows = [] @@ -574,7 +557,7 @@ def seed_technician_affiliation(self): for technician_id in self.ids["technician"]: row_id = new_id() rows.append(( - row_id, maybe(self.ids["company"], p=0.6), technician_id, + row_id, pick(self.ids["company"]), technician_id, pick(["INDEPENDENT", "AFFILIATED", "PARTNER"]), random.random() < 0.9, )) self.ids.setdefault("technician_affiliation", []).extend(r[0] for r in rows) @@ -599,7 +582,7 @@ def seed_technical_course(self): for _ in range(self.n(15)): rows.append(( new_id(), maybe(self.ids["company"], p=0.5), trunc(FAKE.catch_phrase(), 30), - FAKE.text(150), FAKE.url(), + pick_text("business_note", "note"), FAKE.url(), )) self.insert("technical_course", ["id", "fk_company", "title", "information", "link"], rows) @@ -615,22 +598,18 @@ def seed_local_unit(self): for _ in range(self.n(100)): row_id = new_id() rows.append(( - row_id, pick(self.ids["requester"]), maybe(self.ids["address"]), + row_id, pick(self.ids["requester"]), pick(self.ids["address"]), maybe([f"Apto {random.randint(1, 200)}"], p=0.3), pick(["BUILDING", "HOUSE", "COMPLEX"]), )) self.ids.setdefault("local_unit", []).extend(r[0] for r in rows) self.insert("local_unit", ["id", "fk_requester", "fk_address", "complement", "location_type"], rows) - def seed_local_unit_photo(self): - ids = self.seed_media_assets(self.n(60), "units") - rows = [(mid, pick(self.ids["local_unit"])) for mid in ids] - self.insert("local_unit_photo", ["id", "fk_local_unit"], rows) - def seed_unit_specifications(self): rows = [] for _ in range(self.n(80)): rows.append(( - new_id(), pick(self.ids["local_unit"]), FAKE.text(150), FAKE.url(), + new_id(), pick(self.ids["local_unit"]), pick_text("business_note", "note"), + pick_media("unit_specifications_photos")[0], FAKE.date_time_between("-1y", "now"), )) self.insert("unit_specifications", [ @@ -644,19 +623,18 @@ def seed_energy_bill(self): new_id(), pick(self.ids["local_unit"]), FAKE.pydecimal(left_digits=3, right_digits=2, positive=True), FAKE.pydecimal(left_digits=3, right_digits=2, positive=True), - FAKE.image_url(), uuid.uuid4().hex, )) - self.insert("energy_bill", ["id", "fk_local_unit", "consumption", "price", "photo_url", "photo_public_id"], rows) + self.insert("energy_bill", ["id", "fk_local_unit", "consumption", "price"], rows) def seed_technical_project(self): rows = [] for _ in range(self.n(100)): row_id = new_id() - start = maybe([FAKE.date_time_between("-1y", "now")], p=0.8) + start = FAKE.date_time_between("-1y", "now") rows.append(( - row_id, maybe(self.ids["requester"]), maybe(self.ids["local_unit"]), - maybe(["OPEN", "IN_PROGRESS", "COMPLETED", "CANCELED"], p=0.9), start, - (start + timedelta(days=random.randint(5, 90))) if start else None, + row_id, pick(self.ids["requester"]), pick(self.ids["local_unit"]), + pick(["OPEN", "IN_PROGRESS", "COMPLETED", "CANCELED"]), start, + maybe([start + timedelta(days=random.randint(5, 90))], p=0.8), )) self.ids.setdefault("technical_project", []).extend(r[0] for r in rows) self.insert("technical_project", [ @@ -671,7 +649,7 @@ def seed_technical_service(self): status = pick(["OPEN", "IN_PROGRESS", "COMPLETED", "COMPLETED", "CANCELED"]) accepted = status != "OPEN" rows.append(( - row_id, pick(self.ids["technical_project"]), trunc(FAKE.bs(), 200), status, + row_id, pick(self.ids["technical_project"]), pick_text("technical_service_purpose", "purpose"), status, maybe([created + timedelta(days=1)], p=0.6), created, pick(self.ids["users"]) if accepted else None, (created + timedelta(days=1)) if accepted else None, @@ -680,7 +658,7 @@ def seed_technical_service(self): self.ids.setdefault("technical_service", []).extend(r[0] for r in rows) self.insert("technical_service", [ "id", "fk_technical_project", "purpose", "status", "scheduled_date", "created_at", - "fk_accepted_by", "accepted_at", "end_date", + "accepted_by", "accepted_at", "end_date", ], rows) def seed_service_contract(self): @@ -722,7 +700,7 @@ def seed_professional_review(self): rows.append(( new_id(), technician_id, reviewer_id, service_id, FAKE.pydecimal(left_digits=1, right_digits=1, positive=True, max_value=5), - FAKE.text(120), random.random() < 0.95, FAKE.date_time_between("-1y", "now"), + pick_text("review_comment", "comment"), random.random() < 0.95, FAKE.date_time_between("-1y", "now"), )) self.insert("professional_review", [ "id", "fk_professional", "fk_reviewer", "fk_service", "rating", "comment", "active", "created_at", @@ -736,7 +714,7 @@ def seed_proposal(self): rows.append(( row_id, pick(self.ids["requester"]), pick(["AWAITING_SUPPLIER", "AWAITING_REQUESTER", "ACCEPTED", "REJECTED", "CANCELED"]), - maybe([FAKE.text(100)], p=0.5), None, created, + maybe([pick_text("business_note", "note")], p=0.5), None, created, maybe([created + timedelta(days=random.randint(1, 20))], p=0.6), )) self.ids.setdefault("proposal", []).extend(r[0] for r in rows) @@ -763,14 +741,28 @@ def seed_proposal_unit(self): for proposal_item_id in self.ids["proposal_item"]: rows.append(( new_id(), proposal_item_id, pick(self.ids["local_unit"]), random.randint(1, 5), - maybe([FAKE.sentence()], p=0.3), + maybe([pick_text("business_note", "note")], p=0.3), )) self.insert("proposal_unit", ["id", "fk_proposal_item", "fk_local_unit", "quantity", "note"], rows) def recompute_proposal_totals(self): + # fn_proposal_total() nunca existiu no banco (nem em db/core/procedures, + # que esta vazio) -- soma direto via proposal_item x offer. with self.conn.cursor() as cur: - cur.execute("UPDATE proposal SET total_amount = fn_proposal_total(id)") - print(f" proposal.total_amount recalculado via fn_proposal_total()") + cur.execute(""" + UPDATE proposal p + SET total_amount = sub.total + FROM ( + SELECT pi.fk_proposal AS proposal_id, + SUM(pi.quantity * COALESCE(pi.negotiated_price, o.unit_price) + - COALESCE(pi.discount, 0)) AS total + FROM proposal_item pi + JOIN offer o ON o.id = pi.fk_offer + GROUP BY pi.fk_proposal + ) sub + WHERE p.id = sub.proposal_id + """) + print(" proposal.total_amount recalculado (soma de proposal_item x offer)") def seed_flux_log(self): rows = [] @@ -786,67 +778,64 @@ def seed_flux_log(self): self.insert("flux_log", ["id", "fk_user", "action", "created_at"], rows) -USAGE = "uso: python -m scripts.dataload [rows]" -TARGETS = ("core", "auth", "analytics") - - -def main(): - if len(sys.argv) < 2: - sys.exit(USAGE) - - target: str = sys.argv[1] - if target not in TARGETS: - sys.exit(f"target invalido: {target!r}. {USAGE}") - - # rows e a referencia de escala (default: 1000) - rows: int = int(sys.argv[2]) if len(sys.argv) > 2 else 1000 - - scale = rows / 1000.0 +USAGE = "uso: python -m scripts.dataload [rows]" + +# auth_user e as tabelas que dependem dele vivem no banco do api-auth; +# users e tudo o resto vive no banco do api-core -- sao dois bancos +# Postgres fisicamente separados, entao precisam de duas conexoes. A fase +# auth roda primeiro porque users.auth_id referencia os IDs gerados aqui. +AUTH_STEPS = [ + "seed_auth_user", "seed_local_credential", "seed_federated_identity", + "seed_one_time_token", "seed_auth_session", + "seed_refresh_token", "seed_totp_factor", "seed_security_event", "seed_outbox_event", +] + +CORE_STEPS = [ + "seed_address", "seed_contact", "seed_geolocalization", + "seed_users", "seed_person", + "seed_position", "seed_permission", "seed_position_permission", + "seed_business_contact", "seed_company", + "seed_company_plans", "seed_company_positions", "seed_user_company", + "seed_supplier", "seed_subscription", "seed_charge", + "seed_model", "seed_offer", "seed_inventory", + "seed_profession", "seed_technician", "seed_certification", + "seed_professional_registration", "seed_certification_record", + "seed_technician_affiliation", "seed_shift", "seed_technical_course", + "seed_requester", "seed_local_unit", "seed_unit_specifications", "seed_energy_bill", + "seed_technical_project", "seed_technical_service", + "seed_service_contract", "seed_service_executor", "seed_professional_review", + "seed_proposal", "seed_proposal_item", "seed_proposal_unit", "recompute_proposal_totals", + "seed_flux_log", +] + + +def run_phase(seeder: Seeder, target: str, step_names: list[str]) -> None: conn = connect(target) - seeder = Seeder(conn, scale) - - steps = [ - seeder.seed_address, seeder.seed_contact, seeder.seed_geolocalization, - seeder.seed_auth_user, seeder.seed_local_credential, - # users precisa existir antes de auth_session para a trigger de - # DAU (fn_log_access) conseguir resolver fk_auth_user -> users.id. - seeder.seed_users, seeder.seed_person, - seeder.seed_federated_identity, - seeder.seed_one_time_token, seeder.seed_auth_session, seeder.seed_session_authentication_method, - seeder.seed_refresh_token, seeder.seed_totp_factor, seeder.seed_security_event, - seeder.seed_outbox_event, - seeder.seed_position, seeder.seed_permission, - seeder.seed_position_permission, - seeder.seed_business_contact, seeder.seed_company, seeder.seed_company_photo, - seeder.seed_company_plans, seeder.seed_company_positions, seeder.seed_user_company, - seeder.seed_supplier, seeder.seed_subscription, seeder.seed_charge, - seeder.seed_model, seeder.seed_model_photo, seeder.seed_offer, - seeder.seed_offer_service_region, seeder.seed_offer_translation, seeder.seed_inventory, - seeder.seed_profession, seeder.seed_certification, seeder.seed_technician, - seeder.seed_professional_registration, seeder.seed_certification_record, - seeder.seed_technician_affiliation, seeder.seed_shift, seeder.seed_technical_course, - seeder.seed_requester, seeder.seed_local_unit, seeder.seed_local_unit_photo, - seeder.seed_unit_specifications, seeder.seed_energy_bill, - seeder.seed_technical_project, seeder.seed_technical_service, - seeder.seed_service_contract, seeder.seed_service_executor, seeder.seed_professional_review, - seeder.seed_proposal, seeder.seed_proposal_item, seeder.seed_proposal_unit, - seeder.recompute_proposal_totals, - seeder.seed_flux_log, - ] - + seeder.use_connection(conn) try: - for step in steps: - print(f"[seed] {step.__name__} ...") - step() + for name in step_names: + print(f"[seed:{target}] {name} ...") + getattr(seeder, name)() conn.commit() - print("[seed] concluido e commitado.") + print(f"[seed:{target}] concluido e commitado.") except Exception: conn.rollback() - print("[seed] FALHOU, alteracoes revertidas.") + print(f"[seed:{target}] FALHOU, alteracoes revertidas.") raise finally: conn.close() +def main(): + # rows e a referencia de escala (default: 1000) + rows: int = int(sys.argv[1]) if len(sys.argv) > 1 else 1000 + + scale = rows / 1000.0 + seeder = Seeder(scale) + + run_phase(seeder, "auth", AUTH_STEPS) + run_phase(seeder, "core", CORE_STEPS) + + if __name__ == "__main__": main()