173 lines
5.8 KiB
Go
173 lines
5.8 KiB
Go
package paymentapiimp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tech/sendico/pkg/api/http/response"
|
|
"github.com/tech/sendico/pkg/discovery"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
"github.com/tech/sendico/pkg/mlogger"
|
|
"github.com/tech/sendico/pkg/model"
|
|
"github.com/tech/sendico/pkg/mservice"
|
|
"github.com/tech/sendico/pkg/mutil/mzap"
|
|
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
|
|
"github.com/tech/sendico/server/interface/api/sresponse"
|
|
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"go.uber.org/zap"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
const (
|
|
documentsServiceName = "BILLING_DOCUMENTS"
|
|
documentsOperationGet = discovery.OperationDocumentsGet
|
|
documentsDialTimeout = 5 * time.Second
|
|
documentsCallTimeout = 10 * time.Second
|
|
)
|
|
|
|
func (a *PaymentAPI) getActDocument(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
|
orgRef, err := a.oph.GetRef(r)
|
|
if err != nil {
|
|
a.logger.Warn("Failed to parse organization reference for document request", zap.Error(err), mutil.PLog(a.oph, r))
|
|
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
|
|
}
|
|
|
|
ctx := r.Context()
|
|
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, bson.NilObjectID, model.ActionRead)
|
|
if err != nil {
|
|
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
|
|
return response.Auto(a.logger, a.Name(), err)
|
|
}
|
|
if !allowed {
|
|
a.logger.Debug("Access denied when downloading act", mutil.PLog(a.oph, r))
|
|
return response.AccessDenied(a.logger, a.Name(), "payments read permission denied")
|
|
}
|
|
|
|
paymentRef := strings.TrimSpace(r.URL.Query().Get("payment_ref"))
|
|
if paymentRef == "" {
|
|
paymentRef = strings.TrimSpace(r.URL.Query().Get("paymentRef"))
|
|
}
|
|
if paymentRef == "" {
|
|
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "payment_ref is required")
|
|
}
|
|
|
|
if a.discovery == nil {
|
|
return response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "discovery client is not configured")
|
|
}
|
|
|
|
lookupCtx, cancel := context.WithTimeout(ctx, discoveryLookupTimeout)
|
|
defer cancel()
|
|
|
|
lookupResp, err := a.discovery.Lookup(lookupCtx)
|
|
if err != nil {
|
|
a.logger.Warn("Failed to lookup discovery registry", zap.Error(err))
|
|
return response.Auto(a.logger, a.Name(), err)
|
|
}
|
|
|
|
service := findDocumentsService(lookupResp.Services)
|
|
if service == nil {
|
|
return response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "billing documents service unavailable")
|
|
}
|
|
|
|
docResp, err := a.fetchActDocument(ctx, service.InvokeURI, paymentRef)
|
|
if err != nil {
|
|
a.logger.Warn("Failed to fetch act document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
|
return documentErrorResponse(a.logger, a.Name(), err)
|
|
}
|
|
if len(docResp.GetContent()) == 0 {
|
|
return response.Error(a.logger, a.Name(), http.StatusInternalServerError, "empty_document", "document service returned empty payload")
|
|
}
|
|
|
|
filename := strings.TrimSpace(docResp.GetFilename())
|
|
if filename == "" {
|
|
filename = fmt.Sprintf("act_%s.pdf", paymentRef)
|
|
}
|
|
mimeType := strings.TrimSpace(docResp.GetMimeType())
|
|
if mimeType == "" {
|
|
mimeType = "application/pdf"
|
|
}
|
|
|
|
return func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", mimeType)
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
|
w.WriteHeader(http.StatusOK)
|
|
if _, writeErr := w.Write(docResp.GetContent()); writeErr != nil {
|
|
a.logger.Warn("Failed to write document response", zap.Error(writeErr))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *PaymentAPI) fetchActDocument(ctx context.Context, invokeURI, paymentRef string) (*documentsv1.GetDocumentResponse, error) {
|
|
dialCtx, cancel := context.WithTimeout(ctx, documentsDialTimeout)
|
|
defer cancel()
|
|
|
|
conn, err := grpc.DialContext(dialCtx, invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
return nil, merrors.InternalWrap(err, "dial billing documents")
|
|
}
|
|
defer conn.Close()
|
|
|
|
client := documentsv1.NewDocumentServiceClient(conn)
|
|
|
|
callCtx, callCancel := context.WithTimeout(ctx, documentsCallTimeout)
|
|
defer callCancel()
|
|
|
|
return client.GetDocument(callCtx, &documentsv1.GetDocumentRequest{
|
|
PaymentRef: paymentRef,
|
|
Type: documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
|
|
})
|
|
}
|
|
|
|
func findDocumentsService(services []discovery.ServiceSummary) *discovery.ServiceSummary {
|
|
for _, svc := range services {
|
|
if !strings.EqualFold(svc.Service, documentsServiceName) {
|
|
continue
|
|
}
|
|
if !svc.Healthy || strings.TrimSpace(svc.InvokeURI) == "" {
|
|
continue
|
|
}
|
|
if len(svc.Ops) == 0 || hasOperation(svc.Ops, documentsOperationGet) {
|
|
return &svc
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasOperation(ops []string, target string) bool {
|
|
for _, op := range ops {
|
|
if strings.EqualFold(strings.TrimSpace(op), target) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func documentErrorResponse(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc {
|
|
statusErr, ok := status.FromError(err)
|
|
if !ok {
|
|
return response.Internal(logger, source, err)
|
|
}
|
|
|
|
switch statusErr.Code() {
|
|
case codes.InvalidArgument:
|
|
return response.BadRequest(logger, source, "invalid_argument", statusErr.Message())
|
|
case codes.NotFound:
|
|
return response.NotFound(logger, source, statusErr.Message())
|
|
case codes.Unimplemented:
|
|
return response.NotImplemented(logger, source, statusErr.Message())
|
|
case codes.FailedPrecondition:
|
|
return response.Error(logger, source, http.StatusPreconditionFailed, "failed_precondition", statusErr.Message())
|
|
case codes.Unavailable:
|
|
return response.Error(logger, source, http.StatusServiceUnavailable, "service_unavailable", statusErr.Message())
|
|
default:
|
|
return response.Internal(logger, source, err)
|
|
}
|
|
}
|