refactored notificatoin / tgsettle responsibility boundaries
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/gateway/tgsettle/storage"
|
||||
"github.com/tech/sendico/gateway/tgsettle/storage/model"
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
ri "github.com/tech/sendico/pkg/db/repository/index"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
pendingConfirmationsCollection = "pending_confirmations"
|
||||
fieldPendingRequestID = "requestId"
|
||||
fieldPendingMessageID = "messageId"
|
||||
fieldPendingExpiresAt = "expiresAt"
|
||||
)
|
||||
|
||||
type PendingConfirmations struct {
|
||||
logger mlogger.Logger
|
||||
coll *mongo.Collection
|
||||
}
|
||||
|
||||
func NewPendingConfirmations(logger mlogger.Logger, db *mongo.Database) (*PendingConfirmations, error) {
|
||||
if db == nil {
|
||||
return nil, merrors.InvalidArgument("mongo database is nil")
|
||||
}
|
||||
if logger == nil {
|
||||
logger = zap.NewNop()
|
||||
}
|
||||
logger = logger.Named("pending_confirmations").With(zap.String("collection", pendingConfirmationsCollection))
|
||||
|
||||
repo := repository.CreateMongoRepository(db, pendingConfirmationsCollection)
|
||||
if err := repo.CreateIndex(&ri.Definition{
|
||||
Keys: []ri.Key{{Field: fieldPendingRequestID, Sort: ri.Asc}},
|
||||
Unique: true,
|
||||
}); err != nil {
|
||||
logger.Error("Failed to create pending confirmations request_id index", zap.Error(err), zap.String("index_field", fieldPendingRequestID))
|
||||
return nil, err
|
||||
}
|
||||
if err := repo.CreateIndex(&ri.Definition{
|
||||
Keys: []ri.Key{{Field: fieldPendingMessageID, Sort: ri.Asc}},
|
||||
}); err != nil {
|
||||
logger.Error("Failed to create pending confirmations message_id index", zap.Error(err), zap.String("index_field", fieldPendingMessageID))
|
||||
return nil, err
|
||||
}
|
||||
if err := repo.CreateIndex(&ri.Definition{
|
||||
Keys: []ri.Key{{Field: fieldPendingExpiresAt, Sort: ri.Asc}},
|
||||
}); err != nil {
|
||||
logger.Error("Failed to create pending confirmations expires_at index", zap.Error(err), zap.String("index_field", fieldPendingExpiresAt))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p := &PendingConfirmations{
|
||||
logger: logger,
|
||||
coll: db.Collection(pendingConfirmationsCollection),
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *PendingConfirmations) Upsert(ctx context.Context, record *model.PendingConfirmation) error {
|
||||
if record == nil {
|
||||
return merrors.InvalidArgument("pending confirmation is nil", "record")
|
||||
}
|
||||
record.RequestID = strings.TrimSpace(record.RequestID)
|
||||
record.MessageID = strings.TrimSpace(record.MessageID)
|
||||
record.TargetChatID = strings.TrimSpace(record.TargetChatID)
|
||||
record.SourceService = strings.TrimSpace(record.SourceService)
|
||||
record.Rail = strings.TrimSpace(record.Rail)
|
||||
if record.RequestID == "" {
|
||||
return merrors.InvalidArgument("request_id is required", "request_id")
|
||||
}
|
||||
if record.TargetChatID == "" {
|
||||
return merrors.InvalidArgument("target_chat_id is required", "target_chat_id")
|
||||
}
|
||||
if record.ExpiresAt.IsZero() {
|
||||
return merrors.InvalidArgument("expires_at is required", "expires_at")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
createdAt := record.CreatedAt
|
||||
if createdAt.IsZero() {
|
||||
createdAt = now
|
||||
}
|
||||
record.UpdatedAt = now
|
||||
record.CreatedAt = createdAt
|
||||
record.ID = bson.NilObjectID
|
||||
|
||||
// Explicit map avoids accidentally overriding immutable fields from stale callers.
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"messageId": record.MessageID,
|
||||
"targetChatId": record.TargetChatID,
|
||||
"acceptedUserIds": record.AcceptedUserIDs,
|
||||
"requestedMoney": record.RequestedMoney,
|
||||
"sourceService": record.SourceService,
|
||||
"rail": record.Rail,
|
||||
"clarified": record.Clarified,
|
||||
"expiresAt": record.ExpiresAt,
|
||||
"updatedAt": record.UpdatedAt,
|
||||
},
|
||||
"$setOnInsert": bson.M{
|
||||
"createdAt": createdAt,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := p.coll.UpdateOne(ctx, bson.M{fieldPendingRequestID: record.RequestID}, update, options.UpdateOne().SetUpsert(true))
|
||||
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
|
||||
p.logger.Warn("Failed to upsert pending confirmation", zap.Error(err), zap.String("request_id", record.RequestID))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *PendingConfirmations) FindByRequestID(ctx context.Context, requestID string) (*model.PendingConfirmation, error) {
|
||||
requestID = strings.TrimSpace(requestID)
|
||||
if requestID == "" {
|
||||
return nil, merrors.InvalidArgument("request_id is required", "request_id")
|
||||
}
|
||||
var result model.PendingConfirmation
|
||||
err := p.coll.FindOne(ctx, bson.M{fieldPendingRequestID: requestID}).Decode(&result)
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (p *PendingConfirmations) FindByMessageID(ctx context.Context, messageID string) (*model.PendingConfirmation, error) {
|
||||
messageID = strings.TrimSpace(messageID)
|
||||
if messageID == "" {
|
||||
return nil, merrors.InvalidArgument("message_id is required", "message_id")
|
||||
}
|
||||
var result model.PendingConfirmation
|
||||
err := p.coll.FindOne(ctx, bson.M{fieldPendingMessageID: messageID}).Decode(&result)
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (p *PendingConfirmations) MarkClarified(ctx context.Context, requestID string) error {
|
||||
requestID = strings.TrimSpace(requestID)
|
||||
if requestID == "" {
|
||||
return merrors.InvalidArgument("request_id is required", "request_id")
|
||||
}
|
||||
_, err := p.coll.UpdateOne(ctx, bson.M{fieldPendingRequestID: requestID}, bson.M{
|
||||
"$set": bson.M{
|
||||
"clarified": true,
|
||||
"updatedAt": time.Now(),
|
||||
},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *PendingConfirmations) AttachMessage(ctx context.Context, requestID string, messageID string) error {
|
||||
requestID = strings.TrimSpace(requestID)
|
||||
messageID = strings.TrimSpace(messageID)
|
||||
if requestID == "" {
|
||||
return merrors.InvalidArgument("request_id is required", "request_id")
|
||||
}
|
||||
if messageID == "" {
|
||||
return merrors.InvalidArgument("message_id is required", "message_id")
|
||||
}
|
||||
|
||||
filter := bson.M{
|
||||
fieldPendingRequestID: requestID,
|
||||
"$or": []bson.M{
|
||||
{fieldPendingMessageID: bson.M{"$exists": false}},
|
||||
{fieldPendingMessageID: ""},
|
||||
{fieldPendingMessageID: messageID},
|
||||
},
|
||||
}
|
||||
res, err := p.coll.UpdateOne(ctx, filter, bson.M{
|
||||
"$set": bson.M{
|
||||
fieldPendingMessageID: messageID,
|
||||
"updatedAt": time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.MatchedCount == 0 {
|
||||
return merrors.NoData("pending confirmation not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PendingConfirmations) DeleteByRequestID(ctx context.Context, requestID string) error {
|
||||
requestID = strings.TrimSpace(requestID)
|
||||
if requestID == "" {
|
||||
return merrors.InvalidArgument("request_id is required", "request_id")
|
||||
}
|
||||
_, err := p.coll.DeleteOne(ctx, bson.M{fieldPendingRequestID: requestID})
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *PendingConfirmations) ListExpired(ctx context.Context, now time.Time, limit int64) ([]*model.PendingConfirmation, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
filter := bson.M{
|
||||
fieldPendingExpiresAt: bson.M{"$lte": now},
|
||||
}
|
||||
opts := options.Find().SetLimit(limit).SetSort(bson.D{{Key: fieldPendingExpiresAt, Value: 1}})
|
||||
|
||||
cursor, err := p.coll.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
result := make([]*model.PendingConfirmation, 0)
|
||||
for cursor.Next(ctx) {
|
||||
var next model.PendingConfirmation
|
||||
if err := cursor.Decode(&next); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, &next)
|
||||
}
|
||||
if err := cursor.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var _ storage.PendingConfirmationsStore = (*PendingConfirmations)(nil)
|
||||
Reference in New Issue
Block a user