diff --git a/src/main/java/com/seconectai/condocontab/auth/http/AuthMiddleware.java b/src/main/java/com/seconectai/condocontab/auth/http/AuthMiddleware.java index f7919a5..d126e1c 100644 --- a/src/main/java/com/seconectai/condocontab/auth/http/AuthMiddleware.java +++ b/src/main/java/com/seconectai/condocontab/auth/http/AuthMiddleware.java @@ -60,9 +60,13 @@ public final class AuthMiddleware { return; } } - if (empresaBloqueada(empresaId) && !exchange.getRequestURI().getPath().startsWith("/api/financeiro/")) { - HttpResponses.json(exchange, 403, Map.of("erro", "pendencia_financeira", "mensagem", "Seu acesso esta bloqueado por pendencia de pagamento.")); - return; + String cargo = String.valueOf(exchange.getAttribute(ATTR_CARGO)); + boolean isAdmin = "admin_empresa".equals(cargo); + if (empresaBloqueada(empresaId)) { + if (!isAdmin || !exchange.getRequestURI().getPath().startsWith("/api/financeiro/")) { + HttpResponses.json(exchange, 403, Map.of("erro", "pendencia_financeira", "mensagem", "Seu acesso está bloqueado por pendência de pagamento.")); + return; + } } next.handle(exchange); }; diff --git a/src/main/java/com/seconectai/condocontab/financeiro/FinanceiroHandlers.java b/src/main/java/com/seconectai/condocontab/financeiro/FinanceiroHandlers.java index f7d95d8..6a8fd23 100644 --- a/src/main/java/com/seconectai/condocontab/financeiro/FinanceiroHandlers.java +++ b/src/main/java/com/seconectai/condocontab/financeiro/FinanceiroHandlers.java @@ -1,3 +1,3 @@ package com.seconectai.condocontab.financeiro; import com.seconectai.condocontab.auth.http.AuthMiddleware;import com.seconectai.condocontab.config.DataSourceProvider;import com.seconectai.condocontab.http.HttpResponses;import com.sun.net.httpserver.*;import java.io.IOException;import java.sql.*;import java.time.LocalDate;import java.util.*; -public final class FinanceiroHandlers implements HttpHandler{private final DataSourceProvider ds;private final AbacatePayClient abacate;public FinanceiroHandlers(DataSourceProvider ds,AbacatePayClient abacate){this.ds=ds;this.abacate=abacate;}@Override public void handle(HttpExchange ex)throws IOException{try{route(ex);}catch(Exception e){e.printStackTrace(System.err);HttpResponses.json(ex,500,Map.of("erro","erro_financeiro","mensagem",String.valueOf(e.getMessage())));}}private void route(HttpExchange ex)throws Exception{String path=ex.getRequestURI().getPath();String method=ex.getRequestMethod();long emp=(long)ex.getAttribute(AuthMiddleware.ATTR_EMPRESA_ID);if(path.equals("/api/financeiro/periodo-atual")&&"GET".equals(method)){periodoAtual(ex,emp);return;}if(path.equals("/api/financeiro/pagamentos")&&"POST".equals(method)){criarPagamento(ex,emp);return;}if(path.startsWith("/api/financeiro/pagamentos/")&&"GET".equals(method)){statusPagamento(ex,emp,Long.parseLong(path.substring(path.lastIndexOf('/')+1)));return;}HttpResponses.json(ex,404,Map.of("erro","endpoint_nao_encontrado"));}private void periodoAtual(HttpExchange ex,long emp)throws Exception{try(Connection c=ds.getMasterConnection()){long per=periodo(c,emp);Map ass=assinatura(c,emp);Map pag=one(c,"SELECT * FROM pagamentos WHERE empresa_id=? AND periodo_uso_id=? ORDER BY id DESC LIMIT 1",emp,per);Map p=one(c,"SELECT * FROM periodos_uso WHERE id=?",per);Map out=new LinkedHashMap<>();out.put("periodo",p);out.put("assinatura",ass);out.put("pagamento",pag);HttpResponses.json(ex,200,out);}}private void criarPagamento(HttpExchange ex,long emp)throws Exception{try(Connection c=ds.getMasterConnection()){long per=periodo(c,emp);Map pend=one(c,"SELECT * FROM pagamentos WHERE empresa_id=? AND periodo_uso_id=? AND status='pendente' ORDER BY id DESC LIMIT 1",emp,per);if(pend!=null){HttpResponses.json(ex,200,Map.of("pagamento",pend));return;}Map ass=assinatura(c,emp);long valor=((Number)ass.get("valor_mensal_centavos")).longValue();String ext="periodo_"+per+"_empresa_"+emp;var cob=abacate.criarCobranca(valor,"Mensalidade SeConectai",ext);try(PreparedStatement ps=c.prepareStatement("INSERT INTO pagamentos (empresa_id,periodo_uso_id,valor_centavos,abacatepay_charge_id,qr_code_payload,qr_code_base64,status) VALUES (?,?,?,?,?,?,'pendente') RETURNING id")){ps.setLong(1,emp);ps.setLong(2,per);ps.setLong(3,valor);ps.setString(4,cob.chargeId());ps.setString(5,cob.qrCodePayload());ps.setString(6,cob.qrCodeBase64());try(ResultSet rs=ps.executeQuery()){rs.next();HttpResponses.json(ex,201,Map.of("pagamento",one(c,"SELECT * FROM pagamentos WHERE id=?",rs.getLong(1))));}}}}private void statusPagamento(HttpExchange ex,long emp,long id)throws Exception{try(Connection c=ds.getMasterConnection()){Map p=one(c,"SELECT * FROM pagamentos WHERE id=? AND empresa_id=?",id,emp);if(p==null){HttpResponses.json(ex,404,Map.of("erro","pagamento_nao_encontrado"));return;}HttpResponses.json(ex,200,Map.of("pagamento",p));}}private long periodo(Connection c,long emp)throws SQLException{int dia=diaVencimento(c,emp);LocalDate venc=vencimentoAtual(dia);LocalDate inicio=venc.plusDays(1);LocalDate fim=venc.plusMonths(1);LocalDate mes=venc.withDayOfMonth(1);try(PreparedStatement ps=c.prepareStatement("INSERT INTO periodos_uso (empresa_id,mes_referencia,data_inicio,data_fim,data_vencimento,status) VALUES (?,?,?,?,?,'pendente') ON CONFLICT (empresa_id,mes_referencia) DO UPDATE SET data_inicio=EXCLUDED.data_inicio,data_fim=EXCLUDED.data_fim,data_vencimento=EXCLUDED.data_vencimento RETURNING id")){ps.setLong(1,emp);ps.setDate(2,java.sql.Date.valueOf(mes));ps.setDate(3,java.sql.Date.valueOf(inicio));ps.setDate(4,java.sql.Date.valueOf(fim));ps.setDate(5,java.sql.Date.valueOf(venc));try(ResultSet rs=ps.executeQuery()){rs.next();return rs.getLong(1);}}}private LocalDate vencimentoAtual(int dia){LocalDate hoje=LocalDate.now();LocalDate base=hoje.withDayOfMonth(Math.min(dia,hoje.lengthOfMonth()));if(hoje.isBefore(base)){LocalDate anterior=hoje.minusMonths(1);base=anterior.withDayOfMonth(Math.min(dia,anterior.lengthOfMonth()));}return base;}private int diaVencimento(Connection c,long emp)throws SQLException{Mapa=one(c,"SELECT dia_vencimento FROM assinaturas WHERE empresa_id=? AND status='ativa' ORDER BY id DESC LIMIT 1",emp);if(a==null||a.get("dia_vencimento")==null)return 7;int dia=((Number)a.get("dia_vencimento")).intValue();return Math.max(1,Math.min(28,dia));}private Map assinatura(Connection c,long emp)throws SQLException{Mapa=one(c,"SELECT * FROM assinaturas WHERE empresa_id=? AND status='ativa' ORDER BY id DESC LIMIT 1",emp);if(a!=null)return a;try(PreparedStatement ps=c.prepareStatement("INSERT INTO assinaturas (empresa_id,plano_id,valor_mensal_centavos,dia_vencimento,status) VALUES (?,COALESCE((SELECT id FROM planos ORDER BY id LIMIT 1),1),10000,7,'ativa') RETURNING *")){ps.setLong(1,emp);try(ResultSet rs=ps.executeQuery()){rs.next();return map(rs);}}}Map one(Connection c,String sql,Object...params)throws SQLException{try(PreparedStatement ps=c.prepareStatement(sql)){for(int i=0;i map(ResultSet rs)throws SQLException{Mapm=new LinkedHashMap<>();ResultSetMetaData md=rs.getMetaData();for(int i=1;i<=md.getColumnCount();i++)m.put(md.getColumnLabel(i),rs.getObject(i));return m;}} +public final class FinanceiroHandlers implements HttpHandler{private final DataSourceProvider ds;private final AbacatePayClient abacate;public FinanceiroHandlers(DataSourceProvider ds,AbacatePayClient abacate){this.ds=ds;this.abacate=abacate;}@Override public void handle(HttpExchange ex)throws IOException{try{route(ex);}catch(Exception e){e.printStackTrace(System.err);HttpResponses.json(ex,500,Map.of("erro","erro_financeiro","mensagem",String.valueOf(e.getMessage())));}}private void route(HttpExchange ex)throws Exception{String path=ex.getRequestURI().getPath();String method=ex.getRequestMethod();long emp=(long)ex.getAttribute(AuthMiddleware.ATTR_EMPRESA_ID);String cargo=String.valueOf(ex.getAttribute(AuthMiddleware.ATTR_CARGO));if(!"admin_empresa".equals(cargo)){HttpResponses.json(ex,403,Map.of("erro","acesso_negado","mensagem","Apenas o administrador da empresa tem acesso ao financeiro."));return;}if(path.equals("/api/financeiro/periodo-atual")&&"GET".equals(method)){periodoAtual(ex,emp);return;}if(path.equals("/api/financeiro/pagamentos")&&"POST".equals(method)){criarPagamento(ex,emp);return;}if(path.startsWith("/api/financeiro/pagamentos/")&&"GET".equals(method)){statusPagamento(ex,emp,Long.parseLong(path.substring(path.lastIndexOf('/')+1)));return;}HttpResponses.json(ex,404,Map.of("erro","endpoint_nao_encontrado"));}private void periodoAtual(HttpExchange ex,long emp)throws Exception{try(Connection c=ds.getMasterConnection()){long per=periodo(c,emp);Map ass=assinatura(c,emp);Map pag=one(c,"SELECT * FROM pagamentos WHERE empresa_id=? AND periodo_uso_id=? ORDER BY id DESC LIMIT 1",emp,per);Map p=one(c,"SELECT * FROM periodos_uso WHERE id=?",per);Map out=new LinkedHashMap<>();out.put("periodo",p);out.put("assinatura",ass);out.put("pagamento",pag);HttpResponses.json(ex,200,out);}}private void criarPagamento(HttpExchange ex,long emp)throws Exception{try(Connection c=ds.getMasterConnection()){long per=periodo(c,emp);Map pend=one(c,"SELECT * FROM pagamentos WHERE empresa_id=? AND periodo_uso_id=? AND status='pendente' ORDER BY id DESC LIMIT 1",emp,per);if(pend!=null){HttpResponses.json(ex,200,Map.of("pagamento",pend));return;}Map ass=assinatura(c,emp);long valor=((Number)ass.get("valor_mensal_centavos")).longValue();String ext="periodo_"+per+"_empresa_"+emp;var cob=abacate.criarCobranca(valor,"Mensalidade SeConectai",ext);try(PreparedStatement ps=c.prepareStatement("INSERT INTO pagamentos (empresa_id,periodo_uso_id,valor_centavos,abacatepay_charge_id,qr_code_payload,qr_code_base64,status) VALUES (?,?,?,?,?,?,'pendente') RETURNING id")){ps.setLong(1,emp);ps.setLong(2,per);ps.setLong(3,valor);ps.setString(4,cob.chargeId());ps.setString(5,cob.qrCodePayload());ps.setString(6,cob.qrCodeBase64());try(ResultSet rs=ps.executeQuery()){rs.next();HttpResponses.json(ex,201,Map.of("pagamento",one(c,"SELECT * FROM pagamentos WHERE id=?",rs.getLong(1))));}}}}private void statusPagamento(HttpExchange ex,long emp,long id)throws Exception{try(Connection c=ds.getMasterConnection()){Map p=one(c,"SELECT * FROM pagamentos WHERE id=? AND empresa_id=?",id,emp);if(p==null){HttpResponses.json(ex,404,Map.of("erro","pagamento_nao_encontrado"));return;}HttpResponses.json(ex,200,Map.of("pagamento",p));}}private long periodo(Connection c,long emp)throws SQLException{int dia=diaVencimento(c,emp);LocalDate venc=vencimentoAtual(dia);LocalDate inicio=venc.plusDays(1);LocalDate fim=venc.plusMonths(1);LocalDate mes=venc.withDayOfMonth(1);try(PreparedStatement ps=c.prepareStatement("INSERT INTO periodos_uso (empresa_id,mes_referencia,data_inicio,data_fim,data_vencimento,status) VALUES (?,?,?,?,?,'pendente') ON CONFLICT (empresa_id,mes_referencia) DO UPDATE SET data_inicio=EXCLUDED.data_inicio,data_fim=EXCLUDED.data_fim,data_vencimento=EXCLUDED.data_vencimento RETURNING id")){ps.setLong(1,emp);ps.setDate(2,java.sql.Date.valueOf(mes));ps.setDate(3,java.sql.Date.valueOf(inicio));ps.setDate(4,java.sql.Date.valueOf(fim));ps.setDate(5,java.sql.Date.valueOf(venc));try(ResultSet rs=ps.executeQuery()){rs.next();return rs.getLong(1);}}}private LocalDate vencimentoAtual(int dia){LocalDate hoje=LocalDate.now();LocalDate base=hoje.withDayOfMonth(Math.min(dia,hoje.lengthOfMonth()));if(hoje.isBefore(base)){LocalDate anterior=hoje.minusMonths(1);base=anterior.withDayOfMonth(Math.min(dia,anterior.lengthOfMonth()));}return base;}private int diaVencimento(Connection c,long emp)throws SQLException{Mapa=one(c,"SELECT dia_vencimento FROM assinaturas WHERE empresa_id=? AND status='ativa' ORDER BY id DESC LIMIT 1",emp);if(a==null||a.get("dia_vencimento")==null)return 7;int dia=((Number)a.get("dia_vencimento")).intValue();return Math.max(1,Math.min(28,dia));}private Map assinatura(Connection c,long emp)throws SQLException{Mapa=one(c,"SELECT * FROM assinaturas WHERE empresa_id=? AND status='ativa' ORDER BY id DESC LIMIT 1",emp);if(a!=null)return a;try(PreparedStatement ps=c.prepareStatement("INSERT INTO assinaturas (empresa_id,plano_id,valor_mensal_centavos,dia_vencimento,status) VALUES (?,COALESCE((SELECT id FROM planos ORDER BY id LIMIT 1),1),10000,7,'ativa') RETURNING *")){ps.setLong(1,emp);try(ResultSet rs=ps.executeQuery()){rs.next();return map(rs);}}}Map one(Connection c,String sql,Object...params)throws SQLException{try(PreparedStatement ps=c.prepareStatement(sql)){for(int i=0;i map(ResultSet rs)throws SQLException{Mapm=new LinkedHashMap<>();ResultSetMetaData md=rs.getMetaData();for(int i=1;i<=md.getColumnCount();i++)m.put(md.getColumnLabel(i),rs.getObject(i));return m;}} diff --git a/src/main/webapp/cliente/financeiro.js b/src/main/webapp/cliente/financeiro.js index 7914600..5af9a38 100644 --- a/src/main/webapp/cliente/financeiro.js +++ b/src/main/webapp/cliente/financeiro.js @@ -1,3 +1,4 @@ +if(!Auth.isAdminEmpresa()){location.href='/cliente/condominios.html';throw new Error('Acesso restrito');} let pagamento=null,timer=null;const statusEl=document.querySelector('#status'),alertEl=document.querySelector('#finance-alert'),pix=document.querySelector('#pix'),qr=document.querySelector('#qr'),payload=document.querySelector('#payload'); function data(v){if(!v)return'-';const d=new Date(v+'T00:00:00');return Number.isNaN(d.getTime())?String(v):d.toLocaleDateString('pt-BR')} function dataHora(v){if(!v)return'-';const d=new Date(String(v).replace(' ','T'));return Number.isNaN(d.getTime())?String(v):d.toLocaleString('pt-BR')} diff --git a/src/main/webapp/js/api.js b/src/main/webapp/js/api.js index 955b0b9..abfc87c 100644 --- a/src/main/webapp/js/api.js +++ b/src/main/webapp/js/api.js @@ -1 +1 @@ -window.Api={async request(path,options={}){const headers=options.headers||{};const token=Auth.token();if(token)headers.Authorization=`Bearer ${token}`;if(options.body&&!(options.body instanceof FormData))headers['Content-Type']='application/json';const res=await fetch(path,{...options,headers});const text=await res.text();const data=text?JSON.parse(text):{};if(res.status===401){Auth.logout();return}if(res.status===403&&data.erro==='pendencia_financeira'){this.financeBanner(data.mensagem);throw new Error(data.mensagem||'Pendencia financeira')}if(!res.ok)throw new Error(data.mensagem||data.erro||`Erro HTTP ${res.status}`);return data},get(path){return this.request(path)},post(path,body){return this.request(path,{method:'POST',body:body instanceof FormData?body:JSON.stringify(body||{})})},put(path,body){return this.request(path,{method:'PUT',body:JSON.stringify(body||{})})},financeBanner(message){if(document.querySelector('.finance-banner'))return;const banner=document.createElement('div');banner.className='finance-banner';banner.innerHTML=`${message||'Seu acesso esta bloqueado por pendencia de pagamento.'} Regularizar em Financeiro`;document.body.appendChild(banner)}}; \ No newline at end of file +window.Api={async request(path,options={}){const headers=options.headers||{};const token=Auth.token();if(token)headers.Authorization=`Bearer ${token}`;if(options.body&&!(options.body instanceof FormData))headers['Content-Type']='application/json';const res=await fetch(path,{...options,headers});const text=await res.text();const data=text?JSON.parse(text):{};if(res.status===401){Auth.logout();return}if(res.status===403&&data.erro==='pendencia_financeira'){this.financeBanner(data.mensagem);throw new Error(data.mensagem||'Pendência financeira')}if(!res.ok)throw new Error(data.mensagem||data.erro||`Erro HTTP ${res.status}`);return data},get(path){return this.request(path)},post(path,body){return this.request(path,{method:'POST',body:body instanceof FormData?body:JSON.stringify(body||{})})},put(path,body){return this.request(path,{method:'PUT',body:JSON.stringify(body||{})})},financeBanner(message){if(document.querySelector('.finance-banner'))return;const banner=document.createElement('div');banner.className='finance-banner';const msg=message||'Seu acesso está bloqueado por pendência de pagamento.';const isAdmin=typeof Auth!=='undefined'&&Auth.isAdminEmpresa&&Auth.isAdminEmpresa();if(isAdmin){banner.innerHTML=`${msg} Regularizar em Financeiro`}else{banner.innerHTML=msg}document.body.appendChild(banner)}}; \ No newline at end of file diff --git a/src/main/webapp/js/layout.js b/src/main/webapp/js/layout.js index 7769790..b228dcd 100644 --- a/src/main/webapp/js/layout.js +++ b/src/main/webapp/js/layout.js @@ -1 +1 @@ -window.Layout={mount(scope,active){Auth.require(scope);const label=localStorage.getItem('auth_label')||(scope==='master'?'Master':'Escritório');const master=[['Clientes','/master/clientes.html','clientes'],['Planos','/master/planos.html','planos'],['Financeiro','/master/financeiro.html','financeiro'],['Configurações','/master/configuracoes.html','configuracoes'],['Backup','/master/backup.html','backup']];let cliente=[['Cadastro','/cliente/condominios.html','condominios'],['Lembretes','/cliente/lembretes.html','lembretes'],['Assembleia','/cliente/assembleias.html','assembleias'],['Financeiro','/cliente/financeiro.html','financeiro'],['WhatsApp','/cliente/configuracoes.html','configuracoes']];if(scope==='empresa'&&!Auth.isAdminEmpresa())cliente=cliente.filter(x=>!['configuracoes','financeiro'].includes(x[2]));const menu=(scope==='master'?master:cliente).map(([n,h,k])=>`${n}`).join('');document.body.insertAdjacentHTML('afterbegin',``);document.body.classList.add('app');document.querySelector('main')?.classList.add('content')}}; +window.Layout={mount(scope,active){Auth.require(scope);const label=localStorage.getItem('auth_label')||(scope==='master'?'Master':'Escritório');const master=[['Clientes','/master/clientes.html','clientes'],['Planos','/master/planos.html','planos'],['Financeiro','/master/financeiro.html','financeiro'],['Configurações','/master/configuracoes.html','configuracoes'],['Backup','/master/backup.html','backup']];let cliente=[['Cadastro','/cliente/condominios.html','condominios'],['Lembretes','/cliente/lembretes.html','lembretes'],['Assembleia','/cliente/assembleias.html','assembleias'],['Financeiro','/cliente/financeiro.html','financeiro'],['WhatsApp','/cliente/configuracoes.html','configuracoes']];if(scope==='empresa'&&!Auth.isAdminEmpresa()){if(['configuracoes','financeiro'].includes(active)){location.href='/cliente/condominios.html';return}cliente=cliente.filter(x=>!['configuracoes','financeiro'].includes(x[2]))}const menu=(scope==='master'?master:cliente).map(([n,h,k])=>`${n}`).join('');document.body.insertAdjacentHTML('afterbegin',``);document.body.classList.add('app');document.querySelector('main')?.classList.add('content')}};