{"record":{"id":"4bea3c76c9ec2e1d","repo":"signalapp/Signal-Server","slug":"existing-processor-does-not-match","errorCode":null,"errorMessage":"existing processor does not match","messagePattern":"existing processor does not match","errorType":"exception","errorClass":"SubscriptionProcessorConflictException","httpStatus":409,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java","lineNumber":274,"sourceCode":"      final SubscriberCredentials subscriberCredentials,\n      final T subscriptionPaymentProcessor,\n      final ClientPlatform clientPlatform,\n      final ThrowingBiFunction<T, String, R, E> paymentSetupFunction)\n      throws SubscriptionForbiddenException, SubscriptionNotFoundException, SubscriptionProcessorConflictException, E {\n\n    Subscriptions.Record record = this.getSubscriber(subscriberCredentials);\n    if (record.getProcessorCustomer().isEmpty()) {\n      final ProcessorCustomer pc = subscriptionPaymentProcessor\n          .createCustomer(subscriberCredentials.subscriberUser(), clientPlatform);\n      record = subscriptions.setProcessorAndCustomerId(record,\n          new ProcessorCustomer(pc.customerId(), subscriptionPaymentProcessor.getProvider()),\n          Instant.now());\n    }\n    final ProcessorCustomer processorCustomer = record.getProcessorCustomer()\n        .orElseThrow(() -> new UncheckedIOException(new IOException(\"processor must now exist\")));\n\n    if (processorCustomer.processor() != subscriptionPaymentProcessor.getProvider()) {\n      throw new SubscriptionProcessorConflictException(\"existing processor does not match\");\n    }\n    return paymentSetupFunction.apply(subscriptionPaymentProcessor, processorCustomer.customerId());\n  }\n\n  public interface LevelTransitionValidator {\n\n    /**\n     * Check is a level update is valid\n     *\n     * @param oldLevel The current level of the subscription\n     * @param newLevel The proposed updated level of the subscription\n     * @return true if the subscription can be changed from oldLevel to newLevel, otherwise false\n     */\n    boolean isTransitionValid(long oldLevel, long newLevel);\n  }\n\n  /**\n   * Update the subscription level in the payment processor and update the table.","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java#L256-L292","documentation":"addPaymentMethodToCustomer throws SubscriptionProcessorConflictException(\"existing processor does not match\") when the subscription record's stored ProcessorCustomer was created with a different payment provider than the one handling the current request. A subscription is bound to exactly one processor (Stripe, Braintree, Google Play, App Store); you cannot add payment methods from a second processor to it.","triggerScenarios":"Calling addPaymentMethodToCustomer with a subscriptionPaymentProcessor whose getProvider() differs from processorCustomer.processor() stored on the record (e.g. subscription created via Stripe, now adding a Braintree payment method).","commonSituations":"Client switches payment provider mid-subscription without first cancelling/migrating; wrong processor manager selected by configuration for an endpoint; user pays via app store then tries web card payment on the same subscription.","solutions":["Route the request to the processor that matches record.getProcessorCustomer().processor() instead of the configured one.","If the user genuinely wants to switch processors, cancel the existing subscription first, then create a new subscription with the new processor.","Verify server config/env so donation endpoints map to the same processor the client used at subscription creation."],"exampleFix":"// before\nPaymentProvider provider = PaymentProvider.BRAINTREE; // hardcoded\nmanager.addPaymentMethodToCustomer(creds, new BraintreeManager(...), setupFn);\n// after\nPaymentProvider provider = record.getProcessorCustomer().orElseThrow().processor();\nmanager.addPaymentMethodToCustomer(creds, getProcessor(provider), setupFn);","handlingStrategy":"validation","validationCode":"Subscriptions.Record record = manager.getSubscriber(creds);\nPaymentProvider existing = record.getProcessorCustomer().map(ProcessorCustomer::processor).orElse(null);\nif (existing != null && existing != paymentProcessor.getProvider()) {\n  throw new IllegalStateException(\"subscription already uses \" + existing);\n}","typeGuard":"boolean processorMatches(Subscriptions.Record r, PaymentProvider p) {\n  return r.getProcessorCustomer().map(ProcessorCustomer::processor).map(p::equals).orElse(true);\n}","tryCatchPattern":"try {\n  manager.addPaymentMethodToCustomer(creds, processor, setupFn);\n} catch (SubscriptionProcessorConflictException e) {\n  // redirect user to the processor that owns the subscription or offer cancel-and-recreate\n}","preventionTips":["Check the record's processor before presenting payment-method UI to the user","Support only one processor per subscription lifecycle; switch = cancel + recreate","Keep processor routing configuration consistent between client and server environments"],"tags":["payments","subscriptions","processor-conflict"],"backgroundTag":"conflicting-config-options","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}