The recipient of a transfer now gets a push through FCM instead of a
WhatsApp message (docs/prd-point-coin.md F5).
Customers had nowhere to keep FCM tokens: user_devices only holds staff
devices. Migration 000096 adds customer_devices, and the customer app
registers with PUT /customer/devices { device_id, fcm_token, platform,
app_version } after login and whenever FCM refreshes the token, and
unregisters with DELETE /customer/devices/:device_id on logout. A token
belongs to one customer only: registering it takes it away from whoever
had it on that phone before, so they stop getting this customer's
notifications.
The push goes to every device of the recipient after the commit, titled
"EnakPoint masuk" or "EnakCoin masuk", with type WALLET_TRANSFER_IN, the
TRANSFER_IN transaction id, the group id, the currency and the amount in
its data so the app can open it. A retried transfer sends nothing again. It
stays best effort: no device, FCM not configured or FCM failing is logged
and never undoes the transfer.
The app builds one FCM client and shares it between staff notifications
and customer pushes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.0 KiB
Go
59 lines
2.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"apskel-pos-be/internal/constants"
|
|
"apskel-pos-be/internal/contract"
|
|
"apskel-pos-be/internal/processor"
|
|
"apskel-pos-be/internal/repository"
|
|
)
|
|
|
|
// CustomerDeviceService registers the customer app's devices for push notifications.
|
|
type CustomerDeviceService interface {
|
|
Register(ctx context.Context, customerID uuid.UUID, req *contract.RegisterCustomerDeviceRequest) *contract.Response
|
|
Unregister(ctx context.Context, customerID uuid.UUID, deviceID string) *contract.Response
|
|
}
|
|
|
|
type CustomerDeviceServiceImpl struct {
|
|
devices *processor.CustomerDeviceProcessor
|
|
}
|
|
|
|
func NewCustomerDeviceService(devices *processor.CustomerDeviceProcessor) *CustomerDeviceServiceImpl {
|
|
return &CustomerDeviceServiceImpl{devices: devices}
|
|
}
|
|
|
|
func (s *CustomerDeviceServiceImpl) Register(ctx context.Context, customerID uuid.UUID, req *contract.RegisterCustomerDeviceRequest) *contract.Response {
|
|
err := s.devices.Register(ctx, repository.CustomerDevice{
|
|
CustomerID: customerID,
|
|
DeviceID: req.DeviceID,
|
|
FCMToken: req.FCMToken,
|
|
Platform: req.Platform,
|
|
AppVersion: req.AppVersion,
|
|
})
|
|
if err != nil {
|
|
return customerDeviceErrorResponse(err)
|
|
}
|
|
return contract.BuildSuccessResponse(map[string]interface{}{"device_id": req.DeviceID})
|
|
}
|
|
|
|
func (s *CustomerDeviceServiceImpl) Unregister(ctx context.Context, customerID uuid.UUID, deviceID string) *contract.Response {
|
|
if err := s.devices.Unregister(ctx, customerID, deviceID); err != nil {
|
|
return customerDeviceErrorResponse(err)
|
|
}
|
|
return contract.BuildSuccessResponse(map[string]interface{}{"device_id": deviceID})
|
|
}
|
|
|
|
func customerDeviceErrorResponse(err error) *contract.Response {
|
|
code := constants.InternalServerErrorCode
|
|
if errors.Is(err, processor.ErrInvalidCustomerDevice) {
|
|
code = constants.ValidationErrorCode
|
|
}
|
|
return contract.BuildErrorResponse([]*contract.ResponseError{
|
|
contract.NewResponseError(code, constants.RequestEntity, err.Error()),
|
|
})
|
|
}
|