@@ -57,7 +57,14 @@ internal class SubscriptionOperationExecutor(
5757 val startingOp = operations.first()
5858
5959 return if (startingOp is CreateSubscriptionOperation ) {
60- createSubscription(startingOp, operations)
60+ // If the subscription already exists on the backend (non-local id), POSTing
61+ // to /subscriptions with that id is silently a server-side no-op and drops
62+ // the merged enabled/status payload. Dispatch as an update instead.
63+ if (! IDManager .isLocalId(startingOp.subscriptionId)) {
64+ updateExistingSubscriptionFromCreate(startingOp, operations)
65+ } else {
66+ createSubscription(startingOp, operations)
67+ }
6168 } else if (operations.any { it is DeleteSubscriptionOperation }) {
6269 if (operations.size > 1 ) {
6370 throw Exception (" Only supports one operation! Attempted operations:\n $operations " )
@@ -76,6 +83,30 @@ internal class SubscriptionOperationExecutor(
7683 }
7784 }
7885
86+ private suspend fun updateExistingSubscriptionFromCreate (
87+ createOperation : CreateSubscriptionOperation ,
88+ operations : List <Operation >,
89+ ): ExecutionResponse {
90+ // if there are any deletes all operations should be tossed, nothing to do.
91+ if (operations.any { it is DeleteSubscriptionOperation }) {
92+ return ExecutionResponse (ExecutionResult .SUCCESS )
93+ }
94+
95+ val lastUpdateOperation = operations.lastOrNull { it is UpdateSubscriptionOperation } as UpdateSubscriptionOperation ?
96+ val patchOp =
97+ UpdateSubscriptionOperation (
98+ createOperation.appId,
99+ createOperation.onesignalId,
100+ createOperation.externalId,
101+ createOperation.subscriptionId,
102+ createOperation.type,
103+ lastUpdateOperation?.enabled ? : createOperation.enabled,
104+ lastUpdateOperation?.address ? : createOperation.address,
105+ lastUpdateOperation?.status ? : createOperation.status,
106+ )
107+ return updateSubscription(patchOp, listOf (patchOp))
108+ }
109+
79110 private suspend fun createSubscription (
80111 createOperation : CreateSubscriptionOperation ,
81112 operations : List <Operation >,
@@ -91,12 +122,11 @@ internal class SubscriptionOperationExecutor(
91122 val enabled = lastUpdateOperation?.enabled ? : createOperation.enabled
92123 val address = lastUpdateOperation?.address ? : createOperation.address
93124 val status = lastUpdateOperation?.status ? : createOperation.status
94- val subId = if (! IDManager .isLocalId(createOperation.subscriptionId)) createOperation.subscriptionId else null
95125
96126 try {
97127 val subscription =
98128 SubscriptionObject (
99- id = subId ,
129+ id = null ,
100130 convert(createOperation.type),
101131 address,
102132 enabled,
@@ -220,7 +250,30 @@ internal class SubscriptionOperationExecutor(
220250 ) {
221251 return ExecutionResponse (ExecutionResult .FAIL_RETRY , retryAfterSeconds = ex.retryAfterSeconds)
222252 }
223- // toss this, but create an identical CreateSubscriptionOperation to re-create the subscription being updated.
253+ // The original update target no longer exists on the backend, so issue
254+ // a fresh local-id Create. A non-local id here would re-route through
255+ // updateExistingSubscriptionFromCreate -> PATCH -> 404 indefinitely because
256+ // execute() dispatches non-local-id Creates as PATCHes; only a local id
257+ // forces the POST/rebuild-user recovery path that can actually re-create
258+ // the subscription.
259+ val recoveryLocalId = IDManager .createLocalId()
260+ val staleSubscriptionId = lastOperation.subscriptionId
261+
262+ // Rewrite the cached SubscriptionModel id (and pushSubscriptionId, if it
263+ // pointed at the stale id) to the new local id. This keeps the recovery
264+ // Create and the rebuild-user path's getRebuildOperationsIfCurrentUser
265+ // emitting Creates with the same subscriptionId, so they dedupe instead
266+ // of producing two POST /users subscription rows. HYDRATE prevents the
267+ // SubscriptionModelStoreListener from enqueuing follow-on operations.
268+ _subscriptionModelStore .get(staleSubscriptionId)?.setStringProperty(
269+ SubscriptionModel ::id.name,
270+ recoveryLocalId,
271+ ModelChangeTags .HYDRATE ,
272+ )
273+ if (_configModelStore .model.pushSubscriptionId == staleSubscriptionId) {
274+ _configModelStore .model.pushSubscriptionId = recoveryLocalId
275+ }
276+
224277 ExecutionResponse (
225278 ExecutionResult .FAIL_NORETRY ,
226279 operations =
@@ -229,7 +282,7 @@ internal class SubscriptionOperationExecutor(
229282 lastOperation.appId,
230283 lastOperation.onesignalId,
231284 lastOperation.externalId,
232- lastOperation.subscriptionId ,
285+ recoveryLocalId ,
233286 lastOperation.type,
234287 lastOperation.enabled,
235288 lastOperation.address,
0 commit comments