Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: PaymentProcessor to TransitProcessor #2262

Merged
merged 8 commits into from
Aug 2, 2024
2 changes: 1 addition & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ auth_provider_client_id=benefits-oauth-client-id
agency_card_verifier_api_auth_key=server-auth-token
client_private_key='-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1pt0ZoOuPEVPJJS+5r884zcjZLkZZ2GcPwr79XOLDbOi46on\nCa79kjRnhS0VUK96SwUPS0z9J5mDA5LSNL2RoxFb5QGaevnJY828NupzTNdUd0sY\nJK3kRjKUggHWuB55hwJcH/Dx7I3DNH4NL68UAlK+VjwJkfYPrhq/bl5z8ZiurvBa\n5C1mDxhFpcTZlCfxQoas7D1d+uPACF6mEMbQNd3RaIaSREO50NvNywXIIt/OmCiR\nqI7JtOcn4eyh1I4j9WtlbMhRJLfwPMAgY5epTsWcURmhVofF2wVoFbib3JGCfA7t\nz/gmP5YoEKnf/cumKmF3e9LrZb8zwm7bTHUViwIDAQABAoIBAQCIv0XMjNvZS9DC\nXoXGQtVpcxj6dXfaiDgnc7hZDubsNCr3JtT5NqgdIYdVNQUABNDIPNEiCkzFjuwM\nuuF2+dRzM/x6UCs/cSsCjXYBCCOwMwV/fjpEJQnwMQqwTLulVsXZYYeSUtXVBf/8\n0tVULRty34apLFhsyX30UtboXQdESfpmm5ZsqsZJlYljw+M7JxRMneQclI19y/ya\nhPWlfhLB9OffVEJXGaWx1NSYnKoCMKqE/+4krROr6V62xXaNyX6WtU6XiT7C6R5A\nPBxfhmoeFdVCF6a+Qq0v2fKThYoZnV4sn2q2An9YPfynFYnlgzdfnAFSejsqxQd0\nfxYLOtMBAoGBAP1jxjHDJngZ1N+ymw9MIpRgr3HeuMP5phiSTbY2tu9lPzQd+TMX\nfhr1bQh2Fd/vU0u7X0yPnTWtUrLlCdGnWPpXivx95GNGgUUIk2HStFdrRx+f2Qvk\nG8vtLgmSbjQ26UiHzxi9Wa0a41PWIA3TixkcFrS2X29Qc4yd6pVHmicfAoGBANjR\nZ8aaDkSKLkq5Nk1T7I0E1+mtPoH1tPV/FJClXjJrvfDuYHBeOyUpipZddnZuPGWA\nIW2tFIsMgJQtgpvgs52NFI7pQGJRUPK/fTG+Ycocxo78TkLr/RIj8Kj5brXsbZ9P\n3/WBX5GAISTSp1ab8xVgK/Tm07hGupKVqnY2lCAVAoGAIql0YjhE2ecGtLcU+Qm8\nLTnwpg4GjmBnNTNGSCfB7IuYEsQK489R49Qw3xhwM5rkdRajmbCHm+Eiz+/+4NwY\nkt5I1/NMu7vYUR40MwyEuPSm3Q+bvEGu/71pL8wFIUVlshNJ5CN60fA8qqo+5kVK\n4Ntzy7Kq6WpC9Dhh75vE3ZcCgYEAty99uXtxsJD6+aEwcvcENkUwUztPQ6ggAwci\nje9Z/cmwCj6s9mN3HzfQ4qgGrZsHpk4ycCK655xhilBFOIQJ3YRUKUaDYk4H0YDe\nOsf6gTP8wtQDH2GZSNlavLk5w7UFDYQD2b47y4fw+NaOEYvjPl0p5lmb6ebAPZb8\nFbKZRd0CgYBC1HTbA+zMEqDdY4MWJJLC6jZsjdxOGhzjrCtWcIWEGMDF7oDDEoix\nW3j2hwm4C6vaNkH9XX1dr5+q6gq8vJQdbYoExl22BGMiNbfI3+sLRk0zBYL//W6c\ntSREgR4EjosqQfbkceLJ2JT1wuNjInI0eR9H3cRugvlDTeWtbdJ5qA==\n-----END RSA PRIVATE KEY-----'
client_public_key='-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1pt0ZoOuPEVPJJS+5r88\n4zcjZLkZZ2GcPwr79XOLDbOi46onCa79kjRnhS0VUK96SwUPS0z9J5mDA5LSNL2R\noxFb5QGaevnJY828NupzTNdUd0sYJK3kRjKUggHWuB55hwJcH/Dx7I3DNH4NL68U\nAlK+VjwJkfYPrhq/bl5z8ZiurvBa5C1mDxhFpcTZlCfxQoas7D1d+uPACF6mEMbQ\nNd3RaIaSREO50NvNywXIIt/OmCiRqI7JtOcn4eyh1I4j9WtlbMhRJLfwPMAgY5ep\nTsWcURmhVofF2wVoFbib3JGCfA7tz/gmP5YoEKnf/cumKmF3e9LrZb8zwm7bTHUV\niwIDAQAB\n-----END PUBLIC KEY-----'
cst_payment_processor_client_secret=secret
cst_transit_processor_client_secret=secret

testsecret="Hello from the local environment!"
15 changes: 7 additions & 8 deletions benefits/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,15 +90,11 @@ def get_readonly_fields(self, request, obj=None):
return super().get_readonly_fields(request, obj)


@admin.register(models.PaymentProcessor)
class PaymentProcessorAdmin(admin.ModelAdmin): # pragma: no cover
@admin.register(models.TransitProcessor)
class TransitProcessorAdmin(admin.ModelAdmin): # pragma: no cover
def get_exclude(self, request, obj=None):
if not request.user.is_superuser:
return [
"client_id",
"client_secret_name",
"audience",
]
return []
else:
return super().get_exclude(request, obj)

Expand All @@ -121,6 +117,9 @@ def get_exclude(self, request, obj=None):
"private_key",
"public_key",
"jws_signing_alg",
"transit_processor_client_id",
"transit_processor_client_secret_name",
"transit_processor_audience",
]
else:
return super().get_exclude(request, obj)
Expand All @@ -129,7 +128,7 @@ def get_readonly_fields(self, request, obj=None):
if not request.user.is_superuser:
return [
"agency_id",
"payment_processor",
"transit_processor",
"index_template",
"eligibility_index_template",
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Generated by Django 5.0.7 on 2024-07-31 22:41

from django.contrib.auth.management import create_permissions
from django.db import migrations, models

import benefits.core.models
import benefits.secrets


def create_all_permissions(apps, schema_editor):
for app_config in apps.get_app_configs():
app_config.models_module = True
create_permissions(app_config, apps=apps, verbosity=0)
app_config.models_module = None


def update_permissions(apps, schema_editor):
Group = apps.get_model("auth", "Group")
staff_group = Group.objects.get(name="Cal-ITP")

Permission = apps.get_model("auth", "Permission")

remove_permissions = ["Can view", "Can change", "Can add", "Can delete"]
for remove_permission in remove_permissions:
current_permission = Permission.objects.get(name=f"{remove_permission} payment processor")
staff_group.permissions.remove(current_permission)
current_permission.delete()

add_permissions = ["Can view", "Can change"]
for add_permission in add_permissions:
new_permission = Permission.objects.get(name=f"{add_permission} transit processor")
staff_group.permissions.add(new_permission)


def migrate_data(apps, schema_editor):
TransitAgency = apps.get_model("core", "TransitAgency")

for agency in TransitAgency.objects.all():
agency.transit_processor_audience = agency.transit_processor.audience
agency.transit_processor_client_id = agency.transit_processor.client_id
agency.transit_processor_client_secret_name = agency.transit_processor.client_secret_name
agency.save()


class Migration(migrations.Migration):

dependencies = [
("core", "0015_staff_group_edit_permissions"),
]

operations = [
migrations.RenameModel(
old_name="PaymentProcessor",
new_name="TransitProcessor",
),
migrations.RenameField(
model_name="transitagency",
old_name="payment_processor",
new_name="transit_processor",
),
migrations.RunPython(create_all_permissions),
migrations.RunPython(update_permissions),
migrations.AddField(
model_name="transitagency",
name="transit_processor_audience",
field=models.TextField(
default="", help_text="This agency's audience value used to access the TransitProcessor's API."
),
),
migrations.AddField(
model_name="transitagency",
name="transit_processor_client_id",
field=models.TextField(
default="", help_text="This agency's client_id value used to access the TransitProcessor's API."
),
),
migrations.AddField(
model_name="transitagency",
name="transit_processor_client_secret_name",
field=benefits.core.models.SecretNameField(
default="",
help_text="The name of the secret containing this agency's client_secret value used to access the TransitProcessor's API.", # noqa: E501
max_length=127,
validators=[benefits.secrets.SecretNameValidator()],
),
),
migrations.RunPython(migrate_data),
migrations.RemoveField(
model_name="transitprocessor",
name="audience",
),
migrations.RemoveField(
model_name="transitprocessor",
name="client_id",
),
migrations.RemoveField(
model_name="transitprocessor",
name="client_secret_name",
),
migrations.AlterField(
model_name="transitprocessor",
name="api_base_url",
field=models.TextField(help_text="The absolute base URL for the TransitProcessor's API, including https://."),
),
migrations.AlterField(
model_name="transitprocessor",
name="card_tokenize_env",
field=models.TextField(help_text="The environment in which card tokenization is occurring."),
),
migrations.AlterField(
model_name="transitprocessor",
name="card_tokenize_func",
field=models.TextField(
help_text="The function from the card tokenization library to call on the client to initiate the process."
),
),
migrations.AlterField(
model_name="transitprocessor",
name="card_tokenize_url",
field=models.TextField(
help_text="The absolute URL for the client-side card tokenization library provided by the TransitProcessor."
),
),
migrations.AlterField(
model_name="transitprocessor",
name="name",
field=models.TextField(
help_text="Primary internal display name for this TransitProcessor instance, e.g. in the Admin."
),
),
]
12 changes: 6 additions & 6 deletions benefits/core/migrations/local_fixtures.json
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,11 @@
}
},
{
"model": "core.paymentprocessor",
"model": "core.transitprocessor",
"pk": 1,
"fields": {
"name": "(CST) test payment processor",
"name": "(CST) test transit processor",
"api_base_url": "http://server:8000",
"client_id": "",
"client_secret_name": "cst-payment-processor-client-secret",
"audience": "",
"card_tokenize_url": "http://server:8000/static/tokenize.js",
"card_tokenize_func": "tokenize",
"card_tokenize_env": "test"
Expand All @@ -225,7 +222,10 @@
"info_url": "https://www.agency-website.com",
"phone": "1-800-555-5555",
"active": true,
"payment_processor": 1,
"transit_processor": 1,
"transit_processor_client_id": "",
"transit_processor_client_secret_name": "cst-transit-processor-client-secret",
"transit_processor_audience": "",
"private_key": 2,
"public_key": 3,
"jws_signing_alg": "RS256",
Expand Down
41 changes: 26 additions & 15 deletions benefits/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,22 +233,19 @@ def by_id(id):
return EligibilityVerifier.objects.get(id=id)


class PaymentProcessor(models.Model):
"""An entity that processes payments for transit agencies."""
class TransitProcessor(models.Model):
"""An entity that applies transit agency fare rules to rider transactions."""

id = models.AutoField(primary_key=True)
name = models.TextField()
api_base_url = models.TextField()
client_id = models.TextField()
client_secret_name = SecretNameField()
audience = models.TextField()
card_tokenize_url = models.TextField()
card_tokenize_func = models.TextField()
card_tokenize_env = models.TextField()

@property
def client_secret(self):
return get_secret_by_name(self.client_secret_name)
name = models.TextField(help_text="Primary internal display name for this TransitProcessor instance, e.g. in the Admin.")
api_base_url = models.TextField(help_text="The absolute base URL for the TransitProcessor's API, including https://.")
card_tokenize_url = models.TextField(
help_text="The absolute URL for the client-side card tokenization library provided by the TransitProcessor."
)
card_tokenize_func = models.TextField(
help_text="The function from the card tokenization library to call on the client to initiate the process."
)
card_tokenize_env = models.TextField(help_text="The environment in which card tokenization is occurring.")

def __str__(self):
return self.name
Expand All @@ -267,7 +264,17 @@ class TransitAgency(models.Model):
active = models.BooleanField(default=False)
eligibility_types = models.ManyToManyField(EligibilityType)
eligibility_verifiers = models.ManyToManyField(EligibilityVerifier)
payment_processor = models.ForeignKey(PaymentProcessor, on_delete=models.PROTECT)
transit_processor = models.ForeignKey(TransitProcessor, on_delete=models.PROTECT)
transit_processor_client_id = models.TextField(
help_text="This agency's client_id value used to access the TransitProcessor's API.", default=""
)
transit_processor_client_secret_name = SecretNameField(
help_text="The name of the secret containing this agency's client_secret value used to access the TransitProcessor's API.", # noqa: E501
default="",
)
transit_processor_audience = models.TextField(
help_text="This agency's audience value used to access the TransitProcessor's API.", default=""
)
# The Agency's private key, used to sign tokens created on behalf of this Agency
private_key = models.ForeignKey(PemData, related_name="+", on_delete=models.PROTECT)
# The public key corresponding to the Agency's private key, used by Eligibility Verification servers to encrypt responses
Expand Down Expand Up @@ -334,6 +341,10 @@ def active_verifiers(self):
"""This Agency's eligibility verifiers that are active."""
return self.eligibility_verifiers.filter(active=True)

@property
def transit_processor_client_secret(self):
return get_secret_by_name(self.transit_processor_client_secret_name)

@staticmethod
def by_id(id):
"""Get a TransitAgency instance by its ID."""
Expand Down
2 changes: 1 addition & 1 deletion benefits/enrollment/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class ReturnedEnrollmentEvent(core.Event):
"""Analytics event representing the end of payment processor enrollment request."""
"""Analytics event representing the end of transit processor enrollment request."""

def __init__(self, request, status, error=None, payment_group=None):
super().__init__(request, "returned enrollment")
Expand Down
26 changes: 12 additions & 14 deletions benefits/enrollment/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,13 @@ def token(request):
"""View handler for the enrollment auth token."""
if not session.enrollment_token_valid(request):
agency = session.agency(request)
payment_processor = agency.payment_processor

try:
client = Client(
base_url=payment_processor.api_base_url,
client_id=payment_processor.client_id,
client_secret=payment_processor.client_secret,
audience=payment_processor.audience,
base_url=agency.transit_processor.api_base_url,
client_id=agency.transit_processor_client_id,
client_secret=agency.transit_processor_client_secret,
audience=agency.transit_processor_audience,
)
client.oauth.ensure_active_token(client.token)
response = client.request_card_tokenization_access()
Expand Down Expand Up @@ -86,9 +85,8 @@ def index(request):

agency = session.agency(request)
eligibility = session.eligibility(request)
payment_processor = agency.payment_processor

# POST back after payment processor form, process card token
# POST back after transit processor form, process card token
if request.method == "POST":
form = forms.CardTokenizeSuccessForm(request.POST)
if not form.is_valid():
Expand All @@ -97,10 +95,10 @@ def index(request):
card_token = form.cleaned_data.get("card_token")

client = Client(
base_url=payment_processor.api_base_url,
client_id=payment_processor.client_id,
client_secret=payment_processor.client_secret,
audience=payment_processor.audience,
base_url=agency.transit_processor.api_base_url,
client_id=agency.transit_processor_client_id,
client_secret=agency.transit_processor_client_secret,
audience=agency.transit_processor_audience,
)
client.oauth.ensure_active_token(client.token)

Expand Down Expand Up @@ -192,9 +190,9 @@ def index(request):
context = {
"forms": [tokenize_retry_form, tokenize_server_error_form, tokenize_system_error_form, tokenize_success_form],
"cta_button": "tokenize_card",
"card_tokenize_env": agency.payment_processor.card_tokenize_env,
"card_tokenize_func": agency.payment_processor.card_tokenize_func,
"card_tokenize_url": agency.payment_processor.card_tokenize_url,
"card_tokenize_env": agency.transit_processor.card_tokenize_env,
"card_tokenize_func": agency.transit_processor.card_tokenize_func,
"card_tokenize_url": agency.transit_processor.card_tokenize_url,
"token_field": "card_token",
"form_retry": tokenize_retry_form.id,
"form_server_error": tokenize_server_error_form.id,
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ The Benefits application doesn't collect or store any user data directly, and we
Sensitive user information exists in the following places:

- To enroll in a senior discount, users need to [provide personal information to Login.gov](https://benefits.calitp.org/help#login-gov-verify).
- Users need to [provide their credit or debit card information to our payment processor (Littlepay)](https://benefits.calitp.org/help#littlepay) to enroll in a discount.
- Users need to [provide their credit or debit card information to our transit processor (Littlepay)](https://benefits.calitp.org/help#littlepay) to enroll in a discount.

None of that information is accessible to the Benefits system/team.

Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/data.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Some configuration data is not available with the samples in the repository:

- OAuth configuration to enable authentication (read more about [OAuth configuration](oauth.md))
- reCAPTCHA configuration for user-submitted forms
- Payment processor configuration for the enrollment phase
- Transit processor configuration for the enrollment phase
- Amplitude configuration for capturing analytics events

## Rebuilding the configuration database locally
Expand Down
Loading
Loading