source: main/waeup.aaue/trunk/src/waeup/aaue/interswitch/browser.py @ 17514

Last change on this file since 17514 was 17514, checked in by Henrik Bettermann, 17 months ago

Configure LMS Plus Fee single payment.

  • Property svn:executable set to *
File size: 22.9 KB
Line 
1    # -*- coding: utf-8 -*-
2## $Id: browser.py 17429 2023-06-05 04:34:30Z henrik $
3##
4## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
5## This program is free software; you can redistribute it and/or modify
6## it under the terms of the GNU General Public License as published by
7## the Free Software Foundation; either version 2 of the License, or
8## (at your option) any later version.
9##
10## This program is distributed in the hope that it will be useful,
11## but WITHOUT ANY WARRANTY; without even the implied warranty ofself.context.amount_auth
12## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13## GNU General Public License for more details.
14##
15## You should have received a copy of the GNU General Public License
16## along with this program; if not, write to the Free Software
17## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18##
19import httplib
20import hashlib
21import grok
22import os
23import csv
24from xml.dom import minidom
25from zope.interface import Interface
26from zope.component import queryAdapter
27from waeup.kofa.interfaces import CLEARED
28from kofacustom.nigeria.interswitch.browser import (
29    InterswitchPaymentRequestWebservicePageStudent,
30    InterswitchPaymentRequestWebservicePageApplicant,
31    InterswitchPaymentVerifyWebservicePageApplicant,
32    InterswitchPaymentVerifyWebservicePageStudent,
33    InterswitchPageStudent, InterswitchPageApplicant,
34    module_activated,
35    )
36from waeup.aaue.students.interfaces import ICustomStudentOnlinePayment
37from waeup.aaue.applicants.interfaces import ICustomApplicantOnlinePayment
38from waeup.aaue.students.utils import SFEECHANGES
39from waeup.aaue.interfaces import MessageFactory as _
40
41PRODUCT_ID = '5845'
42SITE_NAME = 'aaue.waeup.org'
43PROVIDER_ACCT = '0200244434'
44PROVIDER_BANK_ID = '11'
45PROVIDER_ITEM_NAME = 'WAeAC Portal Fee'
46INSTITUTION_NAME = 'AAU Ekpoma'
47CURRENCY = '566'
48GATEWAY_AMT = 200.0
49POST_ACTION = 'https://webpay.interswitchng.com/paydirect/pay'
50
51HOST = 'webpay.interswitchng.com'
52URL = '/paydirect/api/v1/gettransaction.json'
53HTTPS = True
54MAC = '9718FA00B0F5070B388A9896ADCED9B2FB02D30F71E12E68BDADC63F6852A3496FF97D8A0F9DA9F753B911A49BB09BB87B55FD02046BD325C74C46C0123CF023'
55
56httplib.HTTPSConnection.debuglevel = 0
57
58BANK_ACCOUNTS = {
59    'edohis':    ('1222577132', '117'),
60    'union':     ('1019763348', '7'),
61    'sport':     ('1021941220', '7'),
62    'access':    ('1012688013', '123'),
63    'notebook':  ('4011210501', '51'),
64    'accredit':  ('5060023412', '51'),
65    'library':   ('2000122995', '8'),
66    'fac1':      ('1022438743', '7'),
67    'fac2':      ('2000249757', '8'),
68    'fac3':      ('1012678566', '123'),
69    'matricgown':('2000249757', '8'),
70    'lapel':     ('2000249757', '8'),
71    'lmsplus':   ('5060023429', '51'),
72
73    'acceptance': ('2000249757', '8'),
74
75    'hostel_maintenance': ('1006406795', '123'),
76    'bed_allocation':     ('1006406795', '123'),
77    'late_registration':  ('5210006575', '51'),
78    'ent_combined':       ('6220029828', '51'),
79    'ent_registration_0': ('6220029828', '51'),
80    'ent_registration_1': ('6220029828', '51'),
81    'ent_registration_2': ('6220029828', '51'),
82    'ent_text_book_0':    ('6220029828', '51'),
83    'ent_text_book_1':    ('6220029828', '51'),
84    'ent_text_book_2':    ('6220029828', '51'),
85    'gst_registration_1': ('1010893123', '117'),
86    'gst_registration_2': ('1010893123', '117'),
87    'gst_text_book_0':    ('1010893123', '117'),
88    'gst_text_book_1':    ('1010893123', '117'),
89    'gst_text_book_2':    ('1010893123', '117'),
90    'gst_text_book_3':    ('1010893123', '117'),
91
92    'postgrad': ('1010827641', '117'),
93    }
94
95FEE_NAMES = {
96    'edohis':     'Edo State Health Insurance Scheme',
97    'union':      'Student Union Dues',
98    'sport':      'Sport Development Fee',
99    'access':     'Access Card Fee',
100    'notebook':   'Branded Notebook',
101    'accredit':   'Accreditation Fee',
102    'library':    'Library Development Fee',
103    'tuition':    'Tuition',
104    'acceptance': 'Acceptance Fee',
105    'matricgown': 'Matriculation Gown Fee',
106    'lapel':      'File/Lapel Fee',
107    'lmsplus':    'LMS Plus Fee',
108    'nuga':       'NUGA Fee',
109    }
110
111
112SCHOOLFEES = dict()
113
114for year in SFEECHANGES:
115    schoolfees_path = os.path.join(
116        os.path.dirname(__file__), '../students/schoolfees_%s.csv' %year)
117    reader = csv.DictReader(open(schoolfees_path, 'rb'))
118    SCHOOLFEES[year] = {item['code']:item for item in reader}
119
120acceptancefees_path = os.path.join(
121    os.path.dirname(__file__), '../students/acceptancefees.csv')
122reader = csv.DictReader(open(acceptancefees_path, 'rb'))
123ACCEPTANCEFEES = {item['code']:item for item in reader}
124
125class CustomInterswitchPageApplicant(InterswitchPageApplicant):
126    """ View which sends a POST request to the Interswitch
127    CollegePAY payment gateway.
128
129    So far only PT application has been configured.
130    """
131    grok.context(ICustomApplicantOnlinePayment)
132    action = POST_ACTION
133    site_name = SITE_NAME
134    currency = CURRENCY
135    provider_bank_id = PROVIDER_BANK_ID
136    provider_acct = PROVIDER_ACCT
137    pay_item_id = '101'
138    product_id = PRODUCT_ID
139
140    def update(self):
141        if not module_activated(
142            self.context.__parent__.__parent__.year, self.context):
143            self.flash(_('Forbidden'), type='danger')
144            self.redirect(self.url(self.context, '@@index'))
145            return
146        error = self.init_update()
147        if error:
148            self.flash(error, type='danger')
149            self.redirect(self.url(self.context, '@@index'))
150            return
151        # Already now it becomes an Interswitch payment. We set the net amount
152        # and add the gateway amount.
153        if not self.context.r_company:
154            self.context.net_amt = self.context.amount_auth
155            self.context.amount_auth += GATEWAY_AMT
156            self.context.gateway_amt = GATEWAY_AMT
157            self.context.r_company = u'interswitch'
158        xmldict = {}
159        provider_amt = 2000.0
160        if self.applicant.__parent__.code in ('ver2019', 'send2019'):
161            provider_amt = 0.0
162        elif self.applicant.__parent__.code.startswith('cert'):
163            provider_amt = 3000.0
164        elif self.applicant.__parent__.code.startswith('trans'):
165            provider_amt = 3000.0
166        xmldict['institution_acct'] = '1012332141'
167        xmldict['institution_bank_id'] = '123'
168        if self.applicant.applicant_id.startswith('dsh'):
169            xmldict['institution_acct'] = '1014847058'
170            xmldict['institution_bank_id'] = '7'
171        if self.applicant.applicant_id.startswith('ijmbe'):
172            xmldict['institution_acct'] = '1012278272'
173            xmldict['institution_bank_id'] = '123'
174        xmldict['detail_ref'] = self.context.p_id
175        xmldict['provider_amt'] = 100 * provider_amt
176        xmldict['provider_acct'] = PROVIDER_ACCT
177        xmldict['provider_bank_id'] = PROVIDER_BANK_ID
178        if 'alumni' in self.application_url():
179            xmldict['provider_acct'] = '0427773399'
180            xmldict['provider_bank_id'] = '10'
181        xmldict['provider_item_name'] = PROVIDER_ITEM_NAME
182        xmldict['institution_item_name'] = self.context.category
183        xmldict['institution_name'] = INSTITUTION_NAME
184        xmldict['institution_amt'] = 100 * self.context.net_amt
185        if not self.context.provider_amt:
186            self.context.provider_amt = provider_amt
187            self.context.amount_auth += provider_amt
188        if provider_amt:
189            xmltext = """<payment_item_detail>
190<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s">
191<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
192<item_detail item_id="2" item_name="%(provider_item_name)s" item_amt="%(provider_amt)d" bank_id="%(provider_bank_id)s" acct_num="%(provider_acct)s" />
193</item_details>
194</payment_item_detail>""" % xmldict
195        else:
196            xmltext = """<payment_item_detail>
197<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s">
198<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
199</item_details>
200</payment_item_detail>""" % xmldict
201
202        # Overwrite above configuration
203        if self.applicant.__parent__.code in ('cert1', 'cert2', 'cert6', 'cert9'):
204            xmldict['institution_amt'] = 100 * self.context.net_amt - 100000
205            xmltext = """<payment_item_detail>
206<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s">
207<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
208<item_detail item_id="2" item_name="%(provider_item_name)s" item_amt="%(provider_amt)d" bank_id="%(provider_bank_id)s" acct_num="%(provider_acct)s" />
209<item_detail item_id="3" item_name="Ambrose Alli Foundation" item_amt="100000" bank_id="117" acct_num="1015567975" />
210</item_details>
211</payment_item_detail>""" % xmldict
212
213        self.xml_data = """<input type="hidden" name="xml_data" value='%s'  />""" % xmltext
214        xmlitems = ''
215        xmldoc = minidom.parseString(xmltext)
216        itemlist = xmldoc.getElementsByTagName('item_detail')
217        for s in itemlist:
218            xmlitems += "%s (%s %s, %s (%s)),  " % (
219                s.attributes['item_name'].value,
220                u'\u20a6',
221                int(s.attributes['item_amt'].value)/100,
222                s.attributes['acct_num'].value,
223                s.attributes['bank_id'].value,
224                )
225        self.context.p_split_data = xmlitems
226        self.amount_auth = int(100 * self.context.amount_auth)
227        hashargs = (
228            self.context.p_id +
229            PRODUCT_ID +
230            self.pay_item_id +
231            str(int(self.amount_auth)) +
232            self.site_redirect_url +
233            MAC)
234        self.hashvalue = hashlib.sha512(hashargs).hexdigest()
235        return
236
237class CustomInterswitchPageStudent(InterswitchPageStudent):
238    """ View which sends a POST request to the Interswitch
239    CollegePAY payment gateway.
240    """
241    grok.context(ICustomStudentOnlinePayment)
242    action = POST_ACTION
243    site_name = SITE_NAME
244    currency = CURRENCY
245    pay_item_id = '101'
246    product_id = PRODUCT_ID
247
248    def update(self):
249        if not module_activated(
250            self.context.student.current_session, self.context):
251            self.flash(_('Forbidden'), type='danger')
252            self.redirect(self.url(self.context, '@@index'))
253            return
254        error = self.init_update()
255        if error:
256            self.flash(error, type='danger')
257            self.redirect(self.url(self.context, '@@index'))
258            return
259        student = self.student
260        category = self.context.p_category
261        # To guarantee that cleared students pay both acceptance fee
262        # and school fees, the page can only be accessed
263        # for school fee payments if acceptance/clearance fee has
264        # been successfully queried/paid beforehand. This
265        # requirement applies to students in state 'cleared' and
266        # entry_session greater than 2012 only.
267        if self.context.p_category.startswith('schoolfee') and \
268            student.state == CLEARED and \
269            student.entry_session > 2012:
270            acceptance_fee_paid = False
271            for ticket in student['payments'].values():
272                if ticket.p_state == 'paid' and \
273                    ticket.p_category.startswith('clearance'):
274                    acceptance_fee_paid = True
275                    break
276            if not acceptance_fee_paid:
277                self.flash(
278                    _('Please pay acceptance fee first.'), type="danger")
279                self.redirect(self.url(self.context, '@@index'))
280                return
281        # Already now it becomes an Interswitch payment. We set the net amount
282        # and add the gateway amount.
283        if not self.context.r_company:
284            self.context.net_amt = self.context.amount_auth
285            self.context.amount_auth += GATEWAY_AMT
286            self.context.gateway_amt = GATEWAY_AMT
287            self.context.r_company = u'interswitch'
288        xmldict = self.xmldict
289        # Provider data
290        xmldict['detail_ref'] = self.context.p_id
291        xmldict['provider_acct'] = PROVIDER_ACCT
292        xmldict['provider_bank_id'] = PROVIDER_BANK_ID
293        xmldict['provider_item_name'] = PROVIDER_ITEM_NAME
294        # Institution data
295        xmldict['institution_acct'] = '00000000'
296        xmldict['institution_bank_id'] = '00'
297        provider_amt = 0.0
298        if category.startswith('clearance'):
299            provider_amt = 1500.0
300        elif category.startswith('hostel_maintenance'):
301            provider_amt = 1000.0
302        elif category in ('schoolfee', 'schoolfee_1', 'schoolfee_incl'):
303            provider_amt = 2500.0
304        xmldict['provider_amt'] = 100 * provider_amt
305        xmldict['institution_item_name'] = self.context.category
306        xmldict['institution_name'] = INSTITUTION_NAME
307        xmldict['institution_amt'] = 100 * self.context.net_amt
308        if not self.context.provider_amt:
309            self.context.provider_amt = provider_amt
310            self.context.amount_auth += provider_amt
311        xmltext = ''
312
313        # School fee
314        if category.startswith('schoolfee'):
315            # collect additional fees
316            if self.context.p_category in ('schoolfee_1', 'schoolfee_incl'):
317                xmltext = """<payment_item_detail>
318<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">""" % xmldict
319                item_id = 1
320
321                if student.entry_session < 2013:
322                    sorted_items = SCHOOLFEES[12][student.certcode].items()
323                elif student.entry_session < 2014:
324                    sorted_items = SCHOOLFEES[13][student.certcode].items()
325                elif student.entry_session < 2015:
326                    sorted_items = SCHOOLFEES[14][student.certcode].items()
327                elif student.entry_session < 2020:
328                    sorted_items = SCHOOLFEES[15][student.certcode].items()
329                elif student.entry_session < 2021:
330                    sorted_items = SCHOOLFEES[20][student.certcode].items()
331                elif student.entry_session < 2022:
332                    sorted_items = SCHOOLFEES[21][student.certcode].items()
333                else:
334                    sorted_items = SCHOOLFEES[22][student.certcode].items()
335                sorted_items.insert(0, sorted_items.pop(4))
336                for item in sorted_items:
337                    try:
338                        if  item[1].startswith('100_') and student.state == CLEARED:
339                            # first year payment only
340                            item_amt = 100 * int(item[1].split('_')[1])
341                        else:
342                            item_amt = 100 * int(item[1])
343                        if self.context.p_category == 'schoolfee_1' and item[0] == 'tuition':
344                            item_amt /= 2
345                        acct_num = ''
346                        bank_id = ''
347                        item_name = ''
348                        # Find appropriate bank
349                        try:
350                            bank = BANK_ACCOUNTS[item[0]]
351                        except: # transfer to faculty account
352                            if student.faccode in ('FAG', 'FAT', 'FBM', 'FMLS', 'fac1'):
353                                bank = BANK_ACCOUNTS['fac1']
354                            elif student.faccode in ('FCS', 'FED', 'FES', 'FET'):
355                                bank = BANK_ACCOUNTS['fac2']
356                            elif student.faccode in ('FLS', 'FLW', 'FMS', 'FPS', 'FSS'):
357                                bank = BANK_ACCOUNTS['fac3']
358                        acct_num = bank[0]
359                        bank_id = bank[1]
360                        item_name = FEE_NAMES[item[0]]
361                        xmltext += """
362<item_detail item_id="%s" item_name="%s" item_amt="%d" bank_id="%s" acct_num="%s" />""" % (item_id, item_name, item_amt, bank_id, acct_num)
363                        item_id += 1
364                    except:
365                        pass
366                xmldict['item_id'] = item_id
367                xmltext += """
368<item_detail item_id="%(item_id)d" item_name="%(provider_item_name)s" item_amt="%(provider_amt)d" bank_id="%(provider_bank_id)s" acct_num="%(provider_acct)s" />
369</item_details>
370</payment_item_detail>""" % xmldict
371            # no additional charges, determine faculty bank only
372            else:
373                if student.is_postgrad:
374                    bank = BANK_ACCOUNTS['postgrad']
375                elif student.faccode in ('FAG', 'FAT', 'FBM', 'FMLS', 'fac1'):
376                    bank = BANK_ACCOUNTS['fac1']
377                elif student.faccode in ('FCS', 'FED', 'FES', 'FET'):
378                    bank = BANK_ACCOUNTS['fac2']
379                elif student.faccode in ('FLS', 'FLW', 'FMS', 'FPS', 'FSS'):
380                    bank = BANK_ACCOUNTS['fac3']
381                xmldict['institution_acct'] = bank[0]
382                xmldict['institution_bank_id'] = bank[1]
383
384
385        # Clearance (acceptance) fee
386
387        elif category.startswith('clearance'):
388            # collect additional fees
389            if self.context.p_category == 'clearance_incl':
390                xmltext = """<payment_item_detail>
391<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">""" % xmldict
392                item_id = 1
393                for item in ACCEPTANCEFEES[student.certcode].items():
394                    try:
395                        item_amt = 100 * int(item[1])
396                        bank = BANK_ACCOUNTS[item[0]]
397                        acct_num = bank[0]
398                        bank_id = bank[1]
399                        item_name = FEE_NAMES[item[0]]
400                        xmltext += """
401<item_detail item_id="%s" item_name="%s" item_amt="%d" bank_id="%s" acct_num="%s" />""" % (item_id, item_name, item_amt, bank_id, acct_num)
402                        item_id += 1
403                    except:
404                        pass
405                xmldict['item_id'] = item_id
406                xmltext += """
407<item_detail item_id="%(item_id)d" item_name="%(provider_item_name)s" item_amt="%(provider_amt)d" bank_id="%(provider_bank_id)s" acct_num="%(provider_acct)s" />
408</item_details>
409</payment_item_detail>""" % xmldict
410            # no additional charges, determine faculty bank only
411            else:
412                if student.is_postgrad:
413                    bank = BANK_ACCOUNTS['postgrad']
414                else:
415                    bank = BANK_ACCOUNTS['acceptance']
416                xmldict['institution_acct'] = bank[0]
417                xmldict['institution_bank_id'] = bank[1]
418
419        # Hostel Maintenance
420
421        elif category == 'hostel_maintenance':
422            xmldict['institution_amt'] = 100 * self.context.net_amt - 1000000
423            bank = BANK_ACCOUNTS[category]
424            xmldict['institution_acct'] = bank[0]
425            xmldict['institution_bank_id'] = bank[1]
426            xmltext = """<payment_item_detail>
427<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">
428<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
429<item_detail item_id="2" item_name="%(provider_item_name)s" item_amt="%(provider_amt)d" bank_id="%(provider_bank_id)s" acct_num="%(provider_acct)s" />
430<item_detail item_id="3" item_name="Hostel Consumable Fee" item_amt="1000000" bank_id="123" acct_num="1012332141" />
431</item_details>
432</payment_item_detail>""" % xmldict
433
434        # Other fees
435
436        elif category in BANK_ACCOUNTS.keys():
437            bank = BANK_ACCOUNTS[category]
438            xmldict['institution_acct'] = bank[0]
439            xmldict['institution_bank_id'] = bank[1]
440
441        if not xmltext and provider_amt == 0:
442            xmltext = """<payment_item_detail>
443<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">
444<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
445</item_details>
446</payment_item_detail>""" % xmldict
447        elif not xmltext:
448            xmltext = """<payment_item_detail>
449<item_details detail_ref="%(detail_ref)s" college="%(institution_name)s" department="%(department)s" faculty="%(faculty)s">
450<item_detail item_id="1" item_name="%(institution_item_name)s" item_amt="%(institution_amt)d" bank_id="%(institution_bank_id)s" acct_num="%(institution_acct)s" />
451<item_detail item_id="2" item_name="%(provider_item_name)s" item_amt="%(provider_amt)d" bank_id="%(provider_bank_id)s" acct_num="%(provider_acct)s" />
452</item_details>
453</payment_item_detail>""" % xmldict
454        self.xml_data = """<input type="hidden" name="xml_data" value='%s'  />""" % xmltext
455        xmlitems = ''
456        xmldoc = minidom.parseString(xmltext)
457        itemlist = xmldoc.getElementsByTagName('item_detail')
458        for s in itemlist:
459            xmlitems += "%s (%s %s, %s (%s)),  " % (
460                s.attributes['item_name'].value,
461                u'\u20a6',
462                int(s.attributes['item_amt'].value)/100,
463                s.attributes['acct_num'].value,
464                s.attributes['bank_id'].value,
465                )
466        self.context.p_split_data = xmlitems
467        self.context.provider_amt = provider_amt
468        self.amount_auth = int(100 * self.context.amount_auth)
469        hashargs = (
470            self.context.p_id +
471            PRODUCT_ID +
472            self.pay_item_id +
473            str(int(self.amount_auth)) +
474            self.site_redirect_url +
475            MAC)
476        self.hashvalue = hashlib.sha512(hashargs).hexdigest()
477        return
478
479
480class CustomInterswitchPaymentRequestWebservicePageApplicant(
481    InterswitchPaymentRequestWebservicePageApplicant):
482    """Request webservice view for the CollegePAY gateway
483    """
484    grok.context(ICustomApplicantOnlinePayment)
485    gateway_host = HOST
486    gateway_url = URL
487    https = HTTPS
488    mac = MAC
489    product_id = PRODUCT_ID
490
491class CustomInterswitchPaymentVerifyWebservicePageApplicant(
492    InterswitchPaymentVerifyWebservicePageApplicant):
493    """Payment verify view for the CollegePAY gateway
494    """
495    grok.context(ICustomApplicantOnlinePayment)
496    gateway_host = HOST
497    gateway_url = URL
498    https = HTTPS
499    mac = MAC
500    product_id = PRODUCT_ID
501
502class CustomInterswitchPaymentRequestWebservicePageStudent(
503    InterswitchPaymentRequestWebservicePageStudent):
504    """Request webservice view for the CollegePAY gateway
505    """
506    grok.context(ICustomStudentOnlinePayment)
507    gateway_host = HOST
508    gateway_url = URL
509    https = HTTPS
510    mac = MAC
511    product_id = PRODUCT_ID
512
513class CustomInterswitchPaymentVerifyWebservicePageStudent(
514    InterswitchPaymentVerifyWebservicePageStudent):
515    """Payment verify view for the CollegePAY gateway
516    """
517    grok.context(ICustomStudentOnlinePayment)
518    gateway_host = HOST
519    gateway_url = URL
520    https = HTTPS
521    mac = MAC
522    product_id = PRODUCT_ID
Note: See TracBrowser for help on using the repository browser.