Skip to content

Commit

Permalink
Merge pull request #236 from companieshouse/feature/bi-12789/status_c…
Browse files Browse the repository at this point in the history
…heck

Status Check
  • Loading branch information
emead authored Apr 19, 2023
2 parents dd77d7a + 82afaed commit f652868
Show file tree
Hide file tree
Showing 13 changed files with 724 additions and 8 deletions.
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Config struct {
GovPayBearerTokenChAccount string `env:"GOV_PAY_BEARER_TOKEN_CH_ACCOUNT" flag:"gov-pay-bearer-token-ch-account" flagDesc:"Bearer Token used to authenticate API calls with GovPay for Companies House Payments"`
GovPayBearerTokenLegacy string `env:"GOV_PAY_BEARER_TOKEN_LEGACY" flag:"gov-pay-bearer-token-legacy" flagDesc:"Bearer Token used to authenticate API calls with GovPay for payments on legacy Companies House services"`
GovPaySandbox bool `env:"GOV_PAY_SANDBOX" flag:"gov-pay-sandbox" flagDesc:"Gov Pay Sandbox - returns different refund status values"`
GovPayExpiryTime int `env:"GOV_PAY_EXPIRY_TIME" flag:"gov-pay-expiry_time" flagDesc:"Gov Pay Expiry Time in minutes"`
ExpiryTimeInMinutes string `env:"EXPIRY_TIME_IN_MINUTES" flag:"expiry-time-in-minutes" flagDesc:"The expiry time for the payment session in minutes"`
BrokerAddr []string `env:"KAFKA_BROKER_ADDR" flag:"broker-addr" flagDesc:"Kafka broker address"`
SchemaRegistryURL string `env:"SCHEMA_REGISTRY_URL" flag:"schema-registry-url" flagDesc:"Schema registry url"`
Expand Down
1 change: 1 addition & 0 deletions dao/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type DAO interface {
PatchPaymentResource(id string, paymentUpdate *models.PaymentResourceDB) error
GetPaymentResourceByProviderID(providerID string) (*models.PaymentResourceDB, error)
GetPaymentResourceByExternalPaymentTransactionID(providerID string) (*models.PaymentResourceDB, error)
GetIncompleteGovPayPayments(*config.Config) ([]models.PaymentResourceDB, error)
CreateBulkRefundByProviderID(bulkRefunds map[string]models.BulkRefundDB) error
CreateBulkRefundByExternalPaymentTransactionID(bulkRefunds map[string]models.BulkRefundDB) error
GetPaymentsWithRefundStatus() ([]models.PaymentResourceDB, error)
Expand Down
16 changes: 16 additions & 0 deletions dao/mock_dao.go

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

33 changes: 33 additions & 0 deletions dao/mongo.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/companieshouse/chs.go/log"
"github.com/companieshouse/payments.api.ch.gov.uk/config"
"github.com/companieshouse/payments.api.ch.gov.uk/models"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
Expand Down Expand Up @@ -206,6 +207,38 @@ func (m *MongoService) GetPaymentResourceByExternalPaymentTransactionID(id strin
return &resource, nil
}

// GetIncompleteGovPayPayments retrieves all in-progress payments which have existed longer than the expiry limit
func (m *MongoService) GetIncompleteGovPayPayments(cfg *config.Config) ([]models.PaymentResourceDB, error) {

var pendingPayments []models.PaymentResourceDB

collection := m.db.Collection(m.CollectionName)
now := time.Now()

filter := bson.M{
"data.payment_method": "credit-card",
"data.status": "in-progress",
"data.created_at": bson.M{
"$lt": now.Add(time.Minute * -time.Duration(cfg.GovPayExpiryTime)),
},
}

incompletePaymentsDB, err := collection.Find(context.Background(), filter)
if err != nil {
return nil, err
}

err = incompletePaymentsDB.All(context.Background(), &pendingPayments)
if err != nil {
return nil, err
}

incompletePaymentsDB.Close(context.Background())

return pendingPayments, nil

}

// CreateBulkRefundByProviderID creates or adds to the array of bulk refunds on a payment resource
// The query only updates those payments in the DB with the specified Provider ID
// which do not have an existing bulk refund with the status of refund-pending
Expand Down
50 changes: 50 additions & 0 deletions dao/mongo_driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,56 @@ func TestUnitCreateBulkRefundByExternalPaymentTransactionIDDriver(t *testing.T)

}

func TestUnitGetIncompleteGovPayPaymentsDriver(t *testing.T) {
t.Parallel()

mongoService, commandError, opts, _, _, _ := setDriverUp()

mt := mtest.New(t, opts)
defer mt.Close()

cfg, _ := config.Get()

mt.Run("GetIncompleteGovPayPayments runs successfully", func(mt *mtest.T) {
first := mtest.CreateCursorResponse(1, "models.PaymentResourceDB", mtest.FirstBatch, bson.D{
{"_id", primitive.NewObjectID()},
})

stopCursors := mtest.CreateCursorResponse(0, "models.PaymentResourceDB", mtest.NextBatch)
mt.AddMockResponses(first, stopCursors)

mongoService.db = mt.DB
payments, err := mongoService.GetIncompleteGovPayPayments(cfg)

assert.Nil(t, err)
assert.NotNil(t, payments)
})

mt.Run("GetIncompleteGovPayPayments runs with error on find", func(mt *mtest.T) {
mt.AddMockResponses(mtest.CreateCommandErrorResponse(commandError))

mongoService.db = mt.DB
_, err := mongoService.GetIncompleteGovPayPayments(cfg)

assert.Equal(t, err.Error(), "(Name) Message")
})

mt.Run("GetIncompleteGovPayPayments runs with error on unmarshal cursor", func(mt *mtest.T) {
first := mtest.CreateCursorResponse(1, "models.PaymentResourceDB", mtest.FirstBatch, bson.D{
{"_id", primitive.NewObjectID()},
})

mt.AddMockResponses(first)

mongoService.db = mt.DB
payments, err := mongoService.GetIncompleteGovPayPayments(cfg)

assert.Nil(t, payments)
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "no responses remaining")
})
}

func TestUnitGetPaymentsWithRefundStatusDriver(t *testing.T) {
t.Parallel()

Expand Down
103 changes: 97 additions & 6 deletions handlers/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import (
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/companieshouse/chs.go/log"
"github.com/companieshouse/payments.api.ch.gov.uk/helpers"
"github.com/companieshouse/payments.api.ch.gov.uk/models"
"github.com/companieshouse/payments.api.ch.gov.uk/service"
)

const errorWritingResponse = "error writing response: %w"

// HandleCreatePaymentSession creates a payment session and returns a journey URL for the calling app to redirect to
func HandleCreatePaymentSession(w http.ResponseWriter, req *http.Request) {
if req.Body == nil {
Expand Down Expand Up @@ -47,13 +50,13 @@ func HandleCreatePaymentSession(w http.ResponseWriter, req *http.Request) {
}

// response body contains fully decorated REST model
w.Header().Set("Content-Type", "application/json")
w.Header().Set(contentType, applicationJsonResponseType)
w.Header().Set("Location", paymentResource.Links.Journey)
w.WriteHeader(http.StatusCreated)

err = json.NewEncoder(w).Encode(paymentResource)
if err != nil {
log.ErrorR(req, fmt.Errorf("error writing response: %v", err))
log.ErrorR(req, fmt.Errorf(errorWritingResponse, err))
return
}

Expand Down Expand Up @@ -84,11 +87,11 @@ func HandleGetPaymentSession(w http.ResponseWriter, req *http.Request) {
paymentSession.Status = service.Expired.String()
}

w.Header().Set("Content-Type", "application/json")
w.Header().Set(contentType, applicationJsonResponseType)

err = json.NewEncoder(w).Encode(paymentSession)
if err != nil {
log.ErrorR(req, fmt.Errorf("error writing response: %v", err))
log.ErrorR(req, fmt.Errorf(errorWritingResponse, err))
w.WriteHeader(http.StatusInternalServerError)
return
}
Expand Down Expand Up @@ -191,15 +194,103 @@ func HandleGetPaymentDetails(externalPaymentSvc *service.ExternalPaymentProvider
return
}

w.Header().Set("Content-Type", "application/json")
w.Header().Set(contentType, applicationJsonResponseType)

err = json.NewEncoder(w).Encode(statusResponse)
if err != nil {
log.ErrorR(req, fmt.Errorf("error writing response: %v", err))
log.ErrorR(req, fmt.Errorf(errorWritingResponse, err))
w.WriteHeader(http.StatusInternalServerError)
return
}

log.InfoR(req, "Successful GET request for payment details: ", log.Data{"payment_id": paymentSession.MetaData.ID})
})
}

// HandleCheckPaymentStatus checks the status of incomplete payments and processes appropriately if paid
func HandleCheckPaymentStatus(w http.ResponseWriter, req *http.Request) {
log.InfoR(req, "received request to check payment statuses")

incompletePayments, err := paymentService.GetIncompletePayments(&paymentService.Config)
if err != nil {
log.ErrorR(req, fmt.Errorf("error getting in-progress payments: %w", err))
w.WriteHeader(http.StatusInternalServerError)
return
}

updatedPayments := make([]models.PaymentResourceRest, 0)

if len(*incompletePayments) == 0 {
log.InfoR(req, "no in-progress payments found")
w.Header().Set(contentType, applicationJsonResponseType)
err = json.NewEncoder(w).Encode(updatedPayments)
if err != nil {
log.ErrorR(req, fmt.Errorf(errorWritingResponse, err))
w.WriteHeader(http.StatusInternalServerError)
return
}
return
}

log.InfoR(req, fmt.Sprintf("%d in-progress payments found", len(*incompletePayments)))

// call GovPay for each payment to check status
for _, pendingPayment := range *incompletePayments {

// we need to get session to include costs
paymentSession, _, err := paymentService.GetPaymentSession(req, pendingPayment.MetaData.ID)
if err != nil {
log.ErrorR(req, fmt.Errorf("error getting payment session for paymentID [%s]: [%w]", pendingPayment.MetaData.ID, err))
continue
}
finished, status, providerID, err := externalPaymentService.GovPayService.GetPaymentStatus(paymentSession)
if err != nil {
log.ErrorR(req, fmt.Errorf("error getting status for paymentID [%s]: [%w]", pendingPayment.MetaData.ID, err))
continue
}

if !finished {
log.InfoR(req, fmt.Sprintf("Payment [%s] not finished, skipping.", pendingPayment.MetaData.ID))
continue
}

completedAt := time.Now().Truncate(time.Millisecond)

if status == "paid" {
// payment has been successful, continue processing
err = handlePaymentMessage(pendingPayment.MetaData.ID)
if err != nil {
log.ErrorR(req, fmt.Errorf("error producing payment kafka message for paymentID [%s]: [%w]", pendingPayment.MetaData.ID, err))
continue
}
log.InfoR(req, fmt.Sprintf("kafka message successfully published for paymentID [%s]", pendingPayment.MetaData.ID))
}

paymentSession.Status = status
paymentSession.CompletedAt = completedAt
updatedPayments = append(updatedPayments, *paymentSession)

// update payment status in DB
paymentUpdate := models.PaymentResourceRest{
Status: status,
ProviderID: providerID,
CompletedAt: completedAt,
}

_, err = paymentService.PatchPaymentSession(req, pendingPayment.MetaData.ID, paymentUpdate)
if err != nil {
log.ErrorR(req, fmt.Errorf("error patching DB for paymentID [%s]: [%w]", pendingPayment.MetaData.ID, err))
}
}

w.Header().Set(contentType, applicationJsonResponseType)

err = json.NewEncoder(w).Encode(updatedPayments)
if err != nil {
log.ErrorR(req, fmt.Errorf(errorWritingResponse, err))
w.WriteHeader(http.StatusInternalServerError)
return
}

log.InfoR(req, "finished checking payment statuses")
}
Loading

0 comments on commit f652868

Please sign in to comment.